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
|
# 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
@@ -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
@@ -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" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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`.
|
||||||
@@ -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
|
} // 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;
|
||||||
};
|
};
|
||||||
@@ -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.
@@ -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 <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
|
||||||
@@ -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