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:
dhkatzandClaude Opus 5.5 committed 2026-10-28 05:17:03 -07:00
1 parent 0d126574bf
commit 9a7d7d2908
18 files changed
+491 -57

No files matched your search

+10 -5
View File
@@ -1,7 +1,9 @@
# t5de-renderer # t5de-renderer
A renderer plugin for IMVU Classic (see README.md and docs/interface.md). Part of T5DE; the patches and the patcher A new renderer for IMVU Classic: a 32-bit adapter DLL the client loads as a renderer plugin, and a 64-bit renderer
are in the `t5de` repository (git.t5de.dev/t5de/t5de, usually checked out beside this one). 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 ## Rules
@@ -13,9 +15,12 @@ are in the `t5de` repository (git.t5de.dev/t5de/t5de, usually checked out beside
## Notes ## Notes
- The plugin must match IMVU's ABI: 32-bit, MSVC (cl or clang-cl; MinGW's C++ ABI differs), `__thiscall` - 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, Boost 1.61's `shared_ptr` layout. Exceptions must not cross into IMVU's virtual methods in IMVU's vtable order (never overloaded: MSVC orders overloads unlike their declaration), Boost
code (a different C runtime); the CRT is linked statically. 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 - 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 `%LOCALAPPDATA%\t5de-dev\ghidra`, analysed headless (`support\analyzeHeadless.bat`). The client to analyse is the
`t5de` checkout's `.cache/<version>/install`. `t5de` checkout's `.cache/<version>/install`.
+28 -25
View File
@@ -5,15 +5,18 @@ project(t5de-renderer
DESCRIPTION "A new 3D renderer for the IMVU Classic client" DESCRIPTION "A new 3D renderer for the IMVU Classic client"
LANGUAGES CXX) 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 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.") message(FATAL_ERROR "Build with MSVC or clang-cl: the adapter 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.")
endif() 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>") set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
# Warnings and conformance for our own targets. # 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_options(t5de_options INTERFACE /W4 /permissive- /utf-8 /Zc:__cplusplus)
target_compile_definitions(t5de_options INTERFACE WIN32_LEAN_AND_MEAN NOMINMAX UNICODE _UNICODE) target_compile_definitions(t5de_options INTERFACE WIN32_LEAN_AND_MEAN NOMINMAX UNICODE _UNICODE)
# Our declarations of IMVU's interfaces. # What the adapter and the renderer process share.
add_library(t5de_imvu INTERFACE) add_library(t5de_protocol INTERFACE)
add_library(t5de::imvu ALIAS t5de_imvu) add_library(t5de::protocol ALIAS t5de_protocol)
target_include_directories(t5de_imvu INTERFACE target_include_directories(t5de_protocol INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>) target_compile_features(t5de_protocol INTERFACE cxx_std_20)
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 .)
include(CTest) include(CTest)
if(BUILD_TESTING)
add_subdirectory(tests) 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() endif()
+33 -12
View File
@@ -3,29 +3,43 @@
"cmakeMinimumRequired": { "major": 3, "minor": 28, "patch": 0 }, "cmakeMinimumRequired": { "major": 3, "minor": 28, "patch": 0 },
"configurePresets": [ "configurePresets": [
{ {
"name": "windows-x86", "name": "windows",
"displayName": "Windows, 32-bit (Visual Studio Build Tools)", "hidden": true,
"description": "IMVU Classic is a 32-bit Visual C++ program: build for Win32 with MSVC.",
"generator": "Visual Studio 18 2026", "generator": "Visual Studio 18 2026",
"architecture": { "value": "Win32", "strategy": "set" },
"binaryDir": "${sourceDir}/build/${presetName}", "binaryDir": "${sourceDir}/build/${presetName}",
"installDir": "${sourceDir}/build/${presetName}/install", "installDir": "${sourceDir}/build/install",
"condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" } "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": [ "buildPresets": [
{ "name": "debug", "configurePreset": "windows-x86", "configuration": "Debug" }, { "name": "adapter-debug", "configurePreset": "windows-x86", "configuration": "Debug" },
{ "name": "release", "configurePreset": "windows-x86", "configuration": "Release" } { "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": [ "testPresets": [
{ {
"name": "debug", "name": "adapter-debug",
"configurePreset": "windows-x86", "configurePreset": "windows-x86",
"configuration": "Debug", "configuration": "Debug",
"output": { "outputOnFailure": true } "output": { "outputOnFailure": true }
}, },
{ {
"name": "release", "name": "adapter-release",
"configurePreset": "windows-x86", "configurePreset": "windows-x86",
"configuration": "Release", "configuration": "Release",
"output": { "outputOnFailure": true } "output": { "outputOnFailure": true }
@@ -33,11 +47,18 @@
], ],
"workflowPresets": [ "workflowPresets": [
{ {
"name": "release", "name": "adapter",
"steps": [ "steps": [
{ "type": "configure", "name": "windows-x86" }, { "type": "configure", "name": "windows-x86" },
{ "type": "build", "name": "release" }, { "type": "build", "name": "adapter-release" },
{ "type": "test", "name": "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 # t5de-renderer
A new 3D renderer for the IMVU Classic client, as a renderer plugin the client loads like its own OpenGL renderer. A new 3D renderer for the IMVU Classic client. Part of [T5DE](https://git.t5de.dev/t5de/t5de); unofficial, not
Part of [T5DE](https://git.t5de.dev/t5de/t5de); unofficial, not affiliated with IMVU. 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 Two programs (`docs/architecture.md`): an adapter, `t5deRenderEngine.dll`, which IMVU Classic loads as a renderer
declarations of IMVU's interface (`docs/interface.md`). 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 ## Building
Needs Visual Studio Build Tools 2026 (the C++ workload) and CMake 3.28 or newer. IMVU Classic is a 32-bit Needs Visual Studio Build Tools 2026 (the C++ workload) and CMake 3.28 or newer. One preset per program:
Visual C++ program, so the plugin is built for 32-bit Windows (the `windows-x86` preset):
```sh ```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: The renderer's first configure fetches Diligent Engine (pinned in `renderer/CMakeLists.txt`) and builds it, which
`cmake --preset windows-x86`, `cmake --build --preset debug`, `ctest --preset debug`. takes a while.
## Trying it ## Trying it
Copy `t5deRenderEngine.dll` into a T5DE client folder built with the `t5de-renderer` patch, which has the OpenGL Copy both programs into a T5DE client folder built with the `t5de-renderer` patch, which has the OpenGL setting load
setting load it in place of `glRenderEngine.dll`, then choose OpenGL in Settings > Advanced. The log is the adapter in place of `glRenderEngine.dll`, then choose OpenGL in Settings > Advanced. The adapter's log is
`%TEMP%\t5de-renderer.log`. `%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.
@@ -93,7 +93,9 @@ imvu::Vec2i pixmap_size(const imvu::Pixmap& pixmap)
} // namespace } // 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())); log::info("passthrough: wrapping the engine at {}", static_cast<void*>(inner_.get()));
} }
@@ -335,6 +337,9 @@ void PassthroughEngine::endRendering()
{ {
count(30); count(30);
inner_->endRendering(); inner_->endRendering();
if (renderer_) {
renderer_->frameDrawn();
}
if (logged(frames_)) { if (logged(frames_)) {
std::string line = std::format("frame {}:", frames_); std::string line = std::format("frame {}:", frames_);
for (std::size_t slot = 1; slot < slots; ++slot) { for (std::size_t slot = 1; slot < slots; ++slot) {
@@ -4,14 +4,17 @@
#include <array> #include <array>
#include <cstdint> #include <cstdint>
#include <memory>
#include "renderer_process.hpp"
#include "t5de/imvu/render_engine.hpp" #include "t5de/imvu/render_engine.hpp"
namespace t5de { namespace t5de {
class PassthroughEngine final : public imvu::PiRenderEngine { class PassthroughEngine final : public imvu::PiRenderEngine {
public: 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; ~PassthroughEngine() override;
void resizeFrameBuffer(imvu::Vec2i size) override; void resizeFrameBuffer(imvu::Vec2i size) override;
@@ -65,6 +68,7 @@ private:
void count(std::size_t slot); void count(std::size_t slot);
imvu::SharedPtr<imvu::PiRenderEngine> inner_; imvu::SharedPtr<imvu::PiRenderEngine> inner_;
std::unique_ptr<RendererProcess> renderer_;
std::array<std::uint32_t, slots> calls_{}; std::array<std::uint32_t, slots> calls_{};
std::uint64_t frames_ = 0; std::uint64_t frames_ = 0;
}; };
+5 -1
View File
@@ -7,6 +7,7 @@
#include "log.hpp" #include "log.hpp"
#include "passthrough_engine.hpp" #include "passthrough_engine.hpp"
#include "renderer_process.hpp"
#include "t5de/imvu/render_engine.hpp" #include "t5de/imvu/render_engine.hpp"
namespace { namespace {
@@ -39,11 +40,14 @@ extern "C" imvu::SharedPtr<imvu::PiRenderEngine> __cdecl createRenderEngine(imvu
{ {
try { try {
log::info("createRenderEngine: window {}", static_cast<void*>(window.get())); log::info("createRenderEngine: window {}", static_cast<void*>(window.get()));
HWND hwnd = window ? window->getPlatformWindow() : nullptr;
auto inner = create_imvu_engine(std::move(window)); auto inner = create_imvu_engine(std::move(window));
if (!inner) { if (!inner) {
return {}; 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) { } 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());
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
+47
View File
@@ -0,0 +1,47 @@
# 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. It proves the risky parts: drawing into
the client's window from another process, keeping in step with the client's frames, and mouse input still reaching
the client. Next: the protocol carrying the frame's calls and resources, and the renderer process drawing the scene.
## 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 -1
View File
@@ -5,6 +5,8 @@
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
struct HWND__; // windows.h's window handle type (HWND is HWND__*)
namespace t5de::imvu { namespace t5de::imvu {
template <class T> template <class T>
@@ -94,6 +96,18 @@ struct PointSpriteVector;
// Region lists IMVU passes to texture updates; unknown so far. // Region lists IMVU passes to texture updates; unknown so far.
class CompoundRegion; class CompoundRegion;
class INativeWindow; 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 } // 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;
}