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
@@ -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`.
|
||||
+28
-25
@@ -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(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()
|
||||
+33
-12
@@ -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" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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`.
|
||||
@@ -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 .)
|
||||
File renamed without changes.
File renamed without changes.
@@ -93,7 +93,9 @@ imvu::Vec2i pixmap_size(const imvu::Pixmap& 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()));
|
||||
}
|
||||
@@ -335,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) {
|
||||
@@ -4,14 +4,17 @@
|
||||
|
||||
#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;
|
||||
@@ -65,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;
|
||||
};
|
||||
@@ -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.
@@ -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
|
||||
@@ -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.
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
struct HWND__; // windows.h's window handle type (HWND is HWND__*)
|
||||
|
||||
namespace t5de::imvu {
|
||||
|
||||
template <class T>
|
||||
@@ -94,6 +96,18 @@ struct PointSpriteVector;
|
||||
// Region lists IMVU passes to texture updates; unknown so far.
|
||||
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
|
||||
@@ -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
|
||||
@@ -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 .)
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user