Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c35c8b63a | ||
|
|
9a7d7d2908 | ||
|
|
0d126574bf | ||
|
|
83e0665681 |
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.
@@ -1,5 +1,8 @@
|
|||||||
#include "passthrough_engine.hpp"
|
#include "passthrough_engine.hpp"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "log.hpp"
|
#include "log.hpp"
|
||||||
@@ -49,9 +52,50 @@ constexpr std::array<const char*, PassthroughEngine::slots> names = {
|
|||||||
// Frames whose calls are logged: the first few, then one a minute or so at 30 FPS.
|
// Frames whose calls are logged: the first few, then one a minute or so at 30 FPS.
|
||||||
bool logged(std::uint64_t frame) { return frame < 5 || frame % 1800 == 0; }
|
bool logged(std::uint64_t frame) { return frame < 5 || frame % 1800 == 0; }
|
||||||
|
|
||||||
|
// Whether to log a call's arguments: the first few calls of each method, in the whole process. Enough to check that
|
||||||
|
// our layouts of IMVU's types read sensible values.
|
||||||
|
bool sampled(std::size_t slot)
|
||||||
|
{
|
||||||
|
static std::array<std::atomic<int>, PassthroughEngine::slots> calls{};
|
||||||
|
return calls[slot]++ < 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string text(const imvu::Vec3f& v) { return std::format("({:.3f}, {:.3f}, {:.3f})", v.x, v.y, v.z); }
|
||||||
|
|
||||||
|
std::string text(const imvu::Transform& t)
|
||||||
|
{
|
||||||
|
const auto& m = t.linear;
|
||||||
|
return std::format("[{:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f}] + {}", m[0][0], m[0][1],
|
||||||
|
m[0][2], m[1][0], m[1][1], m[1][2], m[2][0], m[2][1], m[2][2], text(t.translation));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pixmap's size, from pixmap.dll's exported Pixmap::width and Pixmap::height.
|
||||||
|
imvu::Vec2i pixmap_size(const imvu::Pixmap& pixmap)
|
||||||
|
{
|
||||||
|
using Getter = unsigned(__thiscall*)(const imvu::Pixmap*);
|
||||||
|
struct Getters {
|
||||||
|
Getter width;
|
||||||
|
Getter height;
|
||||||
|
};
|
||||||
|
static const Getters getters = [] {
|
||||||
|
HMODULE module = GetModuleHandleW(L"pixmap.dll");
|
||||||
|
if (module == nullptr) {
|
||||||
|
return Getters{};
|
||||||
|
}
|
||||||
|
return Getters{reinterpret_cast<Getter>(GetProcAddress(module, "?width@Pixmap@@QBEIXZ")),
|
||||||
|
reinterpret_cast<Getter>(GetProcAddress(module, "?height@Pixmap@@QBEIXZ"))};
|
||||||
|
}();
|
||||||
|
if (getters.width == nullptr || getters.height == nullptr) {
|
||||||
|
return {-1, -1};
|
||||||
|
}
|
||||||
|
return {static_cast<int>(getters.width(&pixmap)), static_cast<int>(getters.height(&pixmap))};
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // 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()));
|
||||||
}
|
}
|
||||||
@@ -73,26 +117,42 @@ void PassthroughEngine::enable2dFrameBuffer()
|
|||||||
inner_->enable2dFrameBuffer();
|
inner_->enable2dFrameBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PassthroughEngine::clear(std::uint32_t flags, std::uint32_t color)
|
void PassthroughEngine::clear(std::uint32_t flags, imvu::Color color)
|
||||||
{
|
{
|
||||||
|
if (sampled(3)) {
|
||||||
|
log::info("clear(flags {}, color {:08x})", flags, color);
|
||||||
|
}
|
||||||
count(3);
|
count(3);
|
||||||
inner_->clear(flags, color);
|
inner_->clear(flags, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PassthroughEngine::setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection)
|
void PassthroughEngine::setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection)
|
||||||
{
|
{
|
||||||
|
if (sampled(4)) {
|
||||||
|
const auto& m = projection.m;
|
||||||
|
log::info("setProjection({}x{}, [{:.3f} {:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f} {:.3f}; "
|
||||||
|
"{:.3f} {:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f} {:.3f}])",
|
||||||
|
viewport.x, viewport.y, m[0][0], m[0][1], m[0][2], m[0][3], m[1][0], m[1][1], m[1][2],
|
||||||
|
m[1][3], m[2][0], m[2][1], m[2][2], m[2][3], m[3][0], m[3][1], m[3][2], m[3][3]);
|
||||||
|
}
|
||||||
count(4);
|
count(4);
|
||||||
inner_->setProjection(viewport, projection);
|
inner_->setProjection(viewport, projection);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PassthroughEngine::setModelSpace(const imvu::Transform& model)
|
void PassthroughEngine::setModelSpace(const imvu::Transform& model)
|
||||||
{
|
{
|
||||||
|
if (sampled(5)) {
|
||||||
|
log::info("setModelSpace({})", text(model));
|
||||||
|
}
|
||||||
count(5);
|
count(5);
|
||||||
inner_->setModelSpace(model);
|
inner_->setModelSpace(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PassthroughEngine::setViewSpace(const imvu::Transform& view)
|
void PassthroughEngine::setViewSpace(const imvu::Transform& view)
|
||||||
{
|
{
|
||||||
|
if (sampled(6)) {
|
||||||
|
log::info("setViewSpace({})", text(view));
|
||||||
|
}
|
||||||
count(6);
|
count(6);
|
||||||
inner_->setViewSpace(view);
|
inner_->setViewSpace(view);
|
||||||
}
|
}
|
||||||
@@ -117,6 +177,9 @@ void PassthroughEngine::setDepthWritingOn(bool on)
|
|||||||
|
|
||||||
void PassthroughEngine::setBlendMode(imvu::RenderBlendMode mode)
|
void PassthroughEngine::setBlendMode(imvu::RenderBlendMode mode)
|
||||||
{
|
{
|
||||||
|
if (sampled(10)) {
|
||||||
|
log::info("setBlendMode({})", static_cast<int>(mode));
|
||||||
|
}
|
||||||
count(10);
|
count(10);
|
||||||
inner_->setBlendMode(mode);
|
inner_->setBlendMode(mode);
|
||||||
}
|
}
|
||||||
@@ -129,6 +192,9 @@ void PassthroughEngine::setAlphaTestingOn(bool on)
|
|||||||
|
|
||||||
void PassthroughEngine::setAlphaTestValue(float value)
|
void PassthroughEngine::setAlphaTestValue(float value)
|
||||||
{
|
{
|
||||||
|
if (sampled(12)) {
|
||||||
|
log::info("setAlphaTestValue({:.3f})", value);
|
||||||
|
}
|
||||||
count(12);
|
count(12);
|
||||||
inner_->setAlphaTestValue(value);
|
inner_->setAlphaTestValue(value);
|
||||||
}
|
}
|
||||||
@@ -159,6 +225,10 @@ void PassthroughEngine::setLightingOn(bool on)
|
|||||||
|
|
||||||
void PassthroughEngine::setLightProperties(std::uint32_t index, const imvu::LightProperties& light)
|
void PassthroughEngine::setLightProperties(std::uint32_t index, const imvu::LightProperties& light)
|
||||||
{
|
{
|
||||||
|
if (sampled(17)) {
|
||||||
|
log::info("setLightProperties({}, enabled {}, diffuse {:08x}, vector {}, type {})", index,
|
||||||
|
light.enabled, light.diffuse, text(light.vector), static_cast<int>(light.type));
|
||||||
|
}
|
||||||
count(17);
|
count(17);
|
||||||
inner_->setLightProperties(index, light);
|
inner_->setLightProperties(index, light);
|
||||||
}
|
}
|
||||||
@@ -169,8 +239,11 @@ void PassthroughEngine::setFogOn(bool on)
|
|||||||
inner_->setFogOn(on);
|
inner_->setFogOn(on);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PassthroughEngine::setFogAttributes(float start, float end, std::uint32_t color)
|
void PassthroughEngine::setFogAttributes(float start, float end, imvu::Color color)
|
||||||
{
|
{
|
||||||
|
if (sampled(19)) {
|
||||||
|
log::info("setFogAttributes(start {:.3f}, end {:.3f}, color {:08x})", start, end, color);
|
||||||
|
}
|
||||||
count(19);
|
count(19);
|
||||||
inner_->setFogAttributes(start, end, color);
|
inner_->setFogAttributes(start, end, color);
|
||||||
}
|
}
|
||||||
@@ -183,39 +256,61 @@ void PassthroughEngine::setTextureTransformMatrixIdentity(std::uint32_t stage)
|
|||||||
|
|
||||||
void PassthroughEngine::setTextureTransformMatrix(std::uint32_t stage, const imvu::TextureTransform& transform)
|
void PassthroughEngine::setTextureTransformMatrix(std::uint32_t stage, const imvu::TextureTransform& transform)
|
||||||
{
|
{
|
||||||
|
if (sampled(21)) {
|
||||||
|
const auto& m = transform.m;
|
||||||
|
log::info("setTextureTransformMatrix({}, [{:.3f} {:.3f} {:.3f} {:.3f} {:.3f} {:.3f}])", stage, m[0],
|
||||||
|
m[1], m[2], m[3], m[4], m[5]);
|
||||||
|
}
|
||||||
count(21);
|
count(21);
|
||||||
inner_->setTextureTransformMatrix(stage, transform);
|
inner_->setTextureTransformMatrix(stage, transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PassthroughEngine::setEnvironmentAmbient(std::uint32_t color)
|
void PassthroughEngine::setEnvironmentAmbient(imvu::Color color)
|
||||||
{
|
{
|
||||||
|
if (sampled(22)) {
|
||||||
|
log::info("setEnvironmentAmbient({:08x})", color);
|
||||||
|
}
|
||||||
count(22);
|
count(22);
|
||||||
inner_->setEnvironmentAmbient(color);
|
inner_->setEnvironmentAmbient(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
imvu::PiRenderTexture* PassthroughEngine::newRenderTexture(imvu::Pixmap* pixmap, std::uint32_t flags)
|
imvu::PiRenderTexture* PassthroughEngine::newRenderTexture(const imvu::Pixmap& pixmap, std::uint32_t flags)
|
||||||
{
|
{
|
||||||
|
if (sampled(23)) {
|
||||||
|
const auto size = pixmap_size(pixmap);
|
||||||
|
log::info("newRenderTexture({}x{} pixmap, flags {:x})", size.x, size.y, flags);
|
||||||
|
}
|
||||||
count(23);
|
count(23);
|
||||||
return inner_->newRenderTexture(pixmap, flags);
|
return inner_->newRenderTexture(pixmap, flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
imvu::PiInternalIndexBuffer* PassthroughEngine::newIndexBuffer(std::uint32_t indexCount, const std::uint16_t* indices)
|
imvu::PiInternalIndexBuffer* PassthroughEngine::newIndexBuffer(std::uint32_t indexCount, const std::uint16_t* indices)
|
||||||
{
|
{
|
||||||
|
if (sampled(24)) {
|
||||||
|
log::info("newIndexBuffer({} indices)", indexCount);
|
||||||
|
}
|
||||||
count(24);
|
count(24);
|
||||||
return inner_->newIndexBuffer(indexCount, indices);
|
return inner_->newIndexBuffer(indexCount, indices);
|
||||||
}
|
}
|
||||||
|
|
||||||
imvu::PiInternalVertexBuffer* PassthroughEngine::newStaticVertexBuffer(const void* data, std::uint32_t a,
|
imvu::PiInternalVertexBuffer* PassthroughEngine::newStaticVertexBuffer(const void* data, std::uint32_t stride,
|
||||||
std::uint32_t b, std::uint32_t c)
|
std::uint32_t elementSize, std::uint32_t elementCount)
|
||||||
{
|
{
|
||||||
|
if (sampled(25)) {
|
||||||
|
log::info("newStaticVertexBuffer(stride {}, element size {}, {} elements)", stride, elementSize,
|
||||||
|
elementCount);
|
||||||
|
}
|
||||||
count(25);
|
count(25);
|
||||||
return inner_->newStaticVertexBuffer(data, a, b, c);
|
return inner_->newStaticVertexBuffer(data, stride, elementSize, elementCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
imvu::PiInternalVertexBuffer* PassthroughEngine::newDynamicVertexBuffer(std::uint32_t a, std::uint32_t b)
|
imvu::PiInternalVertexBuffer* PassthroughEngine::newDynamicVertexBuffer(std::uint32_t elementSize, std::uint32_t elementCount)
|
||||||
{
|
{
|
||||||
|
if (sampled(26)) {
|
||||||
|
log::info("newDynamicVertexBuffer(element size {}, {} elements)", elementSize, elementCount);
|
||||||
|
}
|
||||||
count(26);
|
count(26);
|
||||||
return inner_->newDynamicVertexBuffer(a, b);
|
return inner_->newDynamicVertexBuffer(elementSize, elementCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
imvu::SharedPtr<imvu::PiPixmapRenderTarget> PassthroughEngine::newPixmapRenderTarget(imvu::Vec2i size)
|
imvu::SharedPtr<imvu::PiPixmapRenderTarget> PassthroughEngine::newPixmapRenderTarget(imvu::Vec2i size)
|
||||||
@@ -242,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) {
|
||||||
@@ -257,15 +355,18 @@ void PassthroughEngine::endRendering()
|
|||||||
}
|
}
|
||||||
|
|
||||||
void PassthroughEngine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
|
void PassthroughEngine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
|
||||||
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords0,
|
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
|
||||||
imvu::PiInternalVertexBuffer* texCoords1)
|
imvu::PiInternalVertexBuffer* colors)
|
||||||
{
|
{
|
||||||
count(31);
|
count(31);
|
||||||
inner_->drawTriangles(indices, positions, normals, texCoords0, texCoords1);
|
inner_->drawTriangles(indices, positions, normals, texCoords, colors);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PassthroughEngine::drawLine(imvu::Vec3f from, imvu::Vec3f to, std::uint32_t color)
|
void PassthroughEngine::drawLine(imvu::Vec3f from, imvu::Vec3f to, std::uint32_t color)
|
||||||
{
|
{
|
||||||
|
if (sampled(32)) {
|
||||||
|
log::info("drawLine({}, {}, {:08x})", text(from), text(to), color);
|
||||||
|
}
|
||||||
count(32);
|
count(32);
|
||||||
inner_->drawLine(from, to, color);
|
inner_->drawLine(from, to, color);
|
||||||
}
|
}
|
||||||
@@ -281,8 +382,12 @@ void PassthroughEngine::drawTrianglesFromMemory(std::uint32_t vertexCount, std::
|
|||||||
const imvu::VertexAttributeArray<imvu::Vec3f>& positions,
|
const imvu::VertexAttributeArray<imvu::Vec3f>& positions,
|
||||||
const imvu::VertexAttributeArray<imvu::Vec3f>& normals,
|
const imvu::VertexAttributeArray<imvu::Vec3f>& normals,
|
||||||
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
|
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
|
||||||
const imvu::VertexAttributeArray<std::uint32_t>& colors)
|
const imvu::VertexAttributeArray<imvu::Color>& colors)
|
||||||
{
|
{
|
||||||
|
if (sampled(34)) {
|
||||||
|
log::info("drawTrianglesFromMemory({} vertices, {} indices; positions {} stride {}, colors {} stride {})",
|
||||||
|
vertexCount, indexCount, positions.count, positions.stride, colors.count, colors.stride);
|
||||||
|
}
|
||||||
count(34);
|
count(34);
|
||||||
inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, colors);
|
inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, colors);
|
||||||
}
|
}
|
||||||
@@ -4,19 +4,22 @@
|
|||||||
|
|
||||||
#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;
|
||||||
void enable2dFrameBuffer() override;
|
void enable2dFrameBuffer() override;
|
||||||
void clear(std::uint32_t flags, std::uint32_t color) override;
|
void clear(std::uint32_t flags, imvu::Color color) override;
|
||||||
void setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection) override;
|
void setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection) override;
|
||||||
void setModelSpace(const imvu::Transform& model) override;
|
void setModelSpace(const imvu::Transform& model) override;
|
||||||
void setViewSpace(const imvu::Transform& view) override;
|
void setViewSpace(const imvu::Transform& view) override;
|
||||||
@@ -32,29 +35,31 @@ public:
|
|||||||
void setLightingOn(bool on) override;
|
void setLightingOn(bool on) override;
|
||||||
void setLightProperties(std::uint32_t index, const imvu::LightProperties& light) override;
|
void setLightProperties(std::uint32_t index, const imvu::LightProperties& light) override;
|
||||||
void setFogOn(bool on) override;
|
void setFogOn(bool on) override;
|
||||||
void setFogAttributes(float start, float end, std::uint32_t color) override;
|
void setFogAttributes(float start, float end, imvu::Color color) override;
|
||||||
void setTextureTransformMatrixIdentity(std::uint32_t stage) override;
|
void setTextureTransformMatrixIdentity(std::uint32_t stage) override;
|
||||||
void setTextureTransformMatrix(std::uint32_t stage, const imvu::TextureTransform& transform) override;
|
void setTextureTransformMatrix(std::uint32_t stage, const imvu::TextureTransform& transform) override;
|
||||||
void setEnvironmentAmbient(std::uint32_t color) override;
|
void setEnvironmentAmbient(imvu::Color color) override;
|
||||||
imvu::PiRenderTexture* newRenderTexture(imvu::Pixmap* pixmap, std::uint32_t flags) override;
|
imvu::PiRenderTexture* newRenderTexture(const imvu::Pixmap& pixmap, std::uint32_t flags) override;
|
||||||
imvu::PiInternalIndexBuffer* newIndexBuffer(std::uint32_t count, const std::uint16_t* indices) override;
|
imvu::PiInternalIndexBuffer* newIndexBuffer(std::uint32_t count, const std::uint16_t* indices) override;
|
||||||
imvu::PiInternalVertexBuffer* newStaticVertexBuffer(const void* data, std::uint32_t a, std::uint32_t b,
|
imvu::PiInternalVertexBuffer* newStaticVertexBuffer(const void* data, std::uint32_t stride,
|
||||||
std::uint32_t c) override;
|
std::uint32_t elementSize,
|
||||||
imvu::PiInternalVertexBuffer* newDynamicVertexBuffer(std::uint32_t a, std::uint32_t b) override;
|
std::uint32_t elementCount) override;
|
||||||
|
imvu::PiInternalVertexBuffer* newDynamicVertexBuffer(std::uint32_t elementSize,
|
||||||
|
std::uint32_t elementCount) override;
|
||||||
imvu::SharedPtr<imvu::PiPixmapRenderTarget> newPixmapRenderTarget(imvu::Vec2i size) override;
|
imvu::SharedPtr<imvu::PiPixmapRenderTarget> newPixmapRenderTarget(imvu::Vec2i size) override;
|
||||||
imvu::SharedPtr<imvu::PiTextureRenderTarget> newTextureRenderTarget(imvu::Vec2i size) override;
|
imvu::SharedPtr<imvu::PiTextureRenderTarget> newTextureRenderTarget(imvu::Vec2i size) override;
|
||||||
void beginRendering(imvu::PiRenderTarget* target) override;
|
void beginRendering(imvu::PiRenderTarget* target) override;
|
||||||
void endRendering() override;
|
void endRendering() override;
|
||||||
void drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
|
void drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
|
||||||
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords0,
|
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
|
||||||
imvu::PiInternalVertexBuffer* texCoords1) override;
|
imvu::PiInternalVertexBuffer* colors) override;
|
||||||
void drawLine(imvu::Vec3f from, imvu::Vec3f to, std::uint32_t color) override;
|
void drawLine(imvu::Vec3f from, imvu::Vec3f to, std::uint32_t color) override;
|
||||||
void drawPointSprites(const imvu::PointSpriteVector& sprites) override;
|
void drawPointSprites(const imvu::PointSpriteVector& sprites) override;
|
||||||
void drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount, const std::uint16_t* indices,
|
void drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount, const std::uint16_t* indices,
|
||||||
const imvu::VertexAttributeArray<imvu::Vec3f>& positions,
|
const imvu::VertexAttributeArray<imvu::Vec3f>& positions,
|
||||||
const imvu::VertexAttributeArray<imvu::Vec3f>& normals,
|
const imvu::VertexAttributeArray<imvu::Vec3f>& normals,
|
||||||
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
|
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
|
||||||
const imvu::VertexAttributeArray<std::uint32_t>& colors) override;
|
const imvu::VertexAttributeArray<imvu::Color>& colors) override;
|
||||||
|
|
||||||
// The interface's methods, by vtable slot (slot 0, the destructor, isn't counted).
|
// The interface's methods, by vtable slot (slot 0, the destructor, isn't counted).
|
||||||
static constexpr std::size_t slots = 35;
|
static constexpr std::size_t slots = 35;
|
||||||
@@ -63,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,52 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
The renderer is two programs:
|
||||||
|
|
||||||
|
- **The adapter**, `t5deRenderEngine.dll` (32-bit, `adapter/`): a renderer plugin IMVU Classic loads in place of its
|
||||||
|
OpenGL renderer (selected by the `t5de-renderer` patch in the `t5de` repository). It implements IMVU's
|
||||||
|
`PiRenderEngine` interface (`include/t5de/imvu/`, `docs/interface.md`) and passes what the client draws to the
|
||||||
|
renderer process. It stays thin: IMVU's process is 32-bit (4 GB of address space shared with Python, Gecko and
|
||||||
|
IMVU's own code) and tied to Visual C++ 2010's ABI.
|
||||||
|
- **The renderer process**, `t5de-renderer.exe` (64-bit, `renderer/`): draws, with
|
||||||
|
[Diligent Engine](https://github.com/DiligentGraphics/DiligentCore) (Apache-2.0). Being 64-bit and separate, it can
|
||||||
|
use any modern API and library, isn't limited by the client's address space, and can crash without taking the
|
||||||
|
client down.
|
||||||
|
|
||||||
|
Why Diligent: one API over Direct3D 11 (every Windows PC, and software rendering on machines without a graphics
|
||||||
|
driver), Direct3D 12 and Vulkan (ray tracing, mesh shaders), and WebGPU (browsers, for a possible future client).
|
||||||
|
It's built with CMake and fetched by `FetchContent`, pinned to a commit (`renderer/CMakeLists.txt`); only the
|
||||||
|
Direct3D 11 backend is built for now.
|
||||||
|
|
||||||
|
## How they talk
|
||||||
|
|
||||||
|
`include/t5de/protocol/` is shared by both builds, so it uses fixed-size fields only. For each engine the client
|
||||||
|
creates (each 3D view, and again on room changes), the adapter:
|
||||||
|
|
||||||
|
1. creates a shared memory channel (`Local\t5de-renderer-<pid>-<n>`) holding IMVU's window handle and a frame
|
||||||
|
count, and an event set after each frame;
|
||||||
|
2. starts `t5de-renderer.exe --channel <name> --parent <pid>` from its own folder;
|
||||||
|
3. sets the event at each `endRendering`, and sets `closing` when the engine is destroyed.
|
||||||
|
|
||||||
|
The renderer process draws into a child window it creates inside the client's window: window handles are valid in
|
||||||
|
every process, and Windows allows child windows across processes (Chrome's GPU process works this way). The child
|
||||||
|
is disabled so mouse input goes to the client's window under it, and the adapter gives the client's window
|
||||||
|
`WS_CLIPCHILDREN` so the client's own drawing stays out of it. The process exits when the engine is destroyed or the
|
||||||
|
client exits.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
A spike: the adapter still has IMVU's Direct3D 9 engine draw the scene (the pass-through), and the renderer process
|
||||||
|
only draws a box in the 3D view's corner, pulsing with the client's frames. Checked in the client (Direct3D 11 in
|
||||||
|
software, on a machine without a graphics driver): the box draws over the scene from the 64-bit process, keeps in
|
||||||
|
step with the client's frames, clicks on it reach the scene, and each renderer process exits with its engine (room
|
||||||
|
changes) and with the client. The client keeps two engines alive in a room, so two processes (about 15 MB each);
|
||||||
|
one process serving all of a client's engines would be leaner.
|
||||||
|
|
||||||
|
Next: the protocol carrying each frame's calls and the resources (buffers, textures), and the renderer process
|
||||||
|
drawing the scene itself, with IMVU's Direct3D engine left as the fallback.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Two presets, one per architecture: `cmake --workflow --preset adapter` (32-bit, with the tests) and
|
||||||
|
`cmake --workflow --preset renderer` (64-bit; the first configure fetches and builds Diligent). Both programs go in
|
||||||
|
the client's folder.
|
||||||
+15
-3
@@ -59,9 +59,21 @@ virtual methods, IMVU's code only uses an engine through helpers that call its v
|
|||||||
`destroy`, `get_deleter`, `get_untyped_deleter`), the use count and the weak count, changed atomically.
|
`destroy`, `get_deleter`, `get_untyped_deleter`), the use count and the weak count, changed atomically.
|
||||||
`include/t5de/imvu/shared_ptr.hpp` matches it. With non-trivial copy and destruction, it's returned through a
|
`include/t5de/imvu/shared_ptr.hpp` matches it. With non-trivial copy and destruction, it's returned through a
|
||||||
hidden pointer and passed on the stack, as IMVU's code expects.
|
hidden pointer and passed on the stack, as IMVU's code expects.
|
||||||
- Still to work out, for a renderer of our own: `Mat4f`, `Transform`, `LightProperties`, `TextureTransform`,
|
- The data types are in `include/t5de/imvu/types.hpp`, worked out from the Direct3D engine: matrices and
|
||||||
`Pixmap` (`pixmap.dll`), the buffers' contents, `VertexAttributeArray<T>`, `std::vector<PointSprite>` as
|
transforms in Direct3D's row-vector convention, colors as `0xAARRGGBB`, lights (point or directional), blend
|
||||||
Visual C++ 2010 lays it out, and `RenderBlendMode`'s values.
|
modes, linear fog, texture coordinate transforms, vertex attribute arrays (data, count, stride). `Pixmap` is used
|
||||||
|
through `pixmap.dll`'s exported methods (`width`, `height`, `pitch`, `pixels`, ...), not its layout.
|
||||||
|
- `drawTriangles`' vertex buffers are positions (`Vec3f`), normals (`Vec3f`), texture coordinates (`Vec2f`) and
|
||||||
|
colors; vertex buffers are made with a stride, element size and count, dynamic ones filled later through the
|
||||||
|
buffer's own `update`. The objects an engine creates (buffers, textures, render targets) are declared in
|
||||||
|
`include/t5de/imvu/resources.hpp`; IMVU's code deletes them through their virtual destructors.
|
||||||
|
- Checked in the client with the pass-through's logged arguments: a right-handed perspective projection (45 degree
|
||||||
|
vertical field of view, Direct3D's 0 to 1 depth) and an orthographic one for 2D, orthonormal view rotations,
|
||||||
|
opaque clear colors, ambient light with zero alpha, linear fog (1 to 50), scrolling texture transforms. Furniture
|
||||||
|
vertices are interleaved: positions and normals are 12-byte elements 32 bytes apart, so strides matter.
|
||||||
|
- Still unknown: point sprites (`drawPointSprites`, particles), render targets, `CompoundRegion` (texture update
|
||||||
|
regions), `newRenderTexture`'s flags, and what `beginRendering` does with a null target. MSVC orders overloaded
|
||||||
|
virtual methods unlike their declaration, so our declarations never overload virtual methods.
|
||||||
- Memory crosses the boundary only through virtual methods (destructors, control blocks), so each side can use its
|
- Memory crosses the boundary only through virtual methods (destructors, control blocks), so each side can use its
|
||||||
own C runtime. Exceptions must not cross it.
|
own C runtime. Exceptions must not cross it.
|
||||||
|
|
||||||
|
|||||||
@@ -1,54 +1,16 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// IMVU's renderer interface, PiRenderEngine (SceneWindow.dll), declared to match its vtable: the order of the methods
|
// IMVU's renderer interface, PiRenderEngine (SceneWindow.dll), declared to match its vtable: the order of the methods
|
||||||
// and their parameter types are what matter. The names are IMVU's, from SceneWindow.dll's exported RenderEngineDx
|
// and their parameter types are what matter. The names are IMVU's, from SceneWindow.dll's exported RenderEngineDx
|
||||||
// methods; parameter names are ours, and guesses until the calls are traced. Types used only through pointers or
|
// methods; parameter names are ours, from what IMVU's Direct3D engine does with them. See docs/interface.md.
|
||||||
// references are left incomplete until we need their layout. See docs/interface.md.
|
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "t5de/imvu/resources.hpp"
|
||||||
#include "t5de/imvu/shared_ptr.hpp"
|
#include "t5de/imvu/shared_ptr.hpp"
|
||||||
|
#include "t5de/imvu/types.hpp"
|
||||||
|
|
||||||
namespace t5de::imvu {
|
namespace t5de::imvu {
|
||||||
|
|
||||||
template <class T>
|
|
||||||
struct Vec2 {
|
|
||||||
T x;
|
|
||||||
T y;
|
|
||||||
};
|
|
||||||
|
|
||||||
template <class T>
|
|
||||||
struct Vec3 {
|
|
||||||
T x;
|
|
||||||
T y;
|
|
||||||
T z;
|
|
||||||
};
|
|
||||||
|
|
||||||
using Vec2i = Vec2<int>;
|
|
||||||
using Vec2f = Vec2<float>;
|
|
||||||
using Vec3f = Vec3<float>;
|
|
||||||
|
|
||||||
struct Mat4f;
|
|
||||||
struct Transform;
|
|
||||||
struct LightProperties;
|
|
||||||
struct TextureTransform;
|
|
||||||
struct PointSprite;
|
|
||||||
class Pixmap;
|
|
||||||
class INativeWindow;
|
|
||||||
class PiRenderTexture;
|
|
||||||
class PiInternalIndexBuffer;
|
|
||||||
class PiInternalVertexBuffer;
|
|
||||||
class PiRenderTarget;
|
|
||||||
class PiPixmapRenderTarget;
|
|
||||||
class PiTextureRenderTarget;
|
|
||||||
template <class T>
|
|
||||||
class VertexAttributeArray;
|
|
||||||
|
|
||||||
// std::vector<PointSprite> as Visual C++ 2010 lays it out.
|
|
||||||
struct PointSpriteVector;
|
|
||||||
|
|
||||||
// Values not yet known.
|
|
||||||
enum class RenderBlendMode : int {};
|
|
||||||
|
|
||||||
class PiRenderEngine {
|
class PiRenderEngine {
|
||||||
public:
|
public:
|
||||||
PiRenderEngine() = default;
|
PiRenderEngine() = default;
|
||||||
@@ -59,7 +21,7 @@ public:
|
|||||||
|
|
||||||
virtual void resizeFrameBuffer(Vec2i size) = 0;
|
virtual void resizeFrameBuffer(Vec2i size) = 0;
|
||||||
virtual void enable2dFrameBuffer() = 0;
|
virtual void enable2dFrameBuffer() = 0;
|
||||||
virtual void clear(std::uint32_t flags, std::uint32_t color) = 0;
|
virtual void clear(std::uint32_t flags, Color color) = 0; // ClearFlags
|
||||||
virtual void setProjection(Vec2i viewport, const Mat4f& projection) = 0;
|
virtual void setProjection(Vec2i viewport, const Mat4f& projection) = 0;
|
||||||
virtual void setModelSpace(const Transform& model) = 0; // slot 5
|
virtual void setModelSpace(const Transform& model) = 0; // slot 5
|
||||||
virtual void setViewSpace(const Transform& view) = 0;
|
virtual void setViewSpace(const Transform& view) = 0;
|
||||||
@@ -75,29 +37,33 @@ public:
|
|||||||
virtual void setLightingOn(bool on) = 0;
|
virtual void setLightingOn(bool on) = 0;
|
||||||
virtual void setLightProperties(std::uint32_t index, const LightProperties& light) = 0;
|
virtual void setLightProperties(std::uint32_t index, const LightProperties& light) = 0;
|
||||||
virtual void setFogOn(bool on) = 0;
|
virtual void setFogOn(bool on) = 0;
|
||||||
virtual void setFogAttributes(float start, float end, std::uint32_t color) = 0;
|
virtual void setFogAttributes(float start, float end, Color color) = 0;
|
||||||
virtual void setTextureTransformMatrixIdentity(std::uint32_t stage) = 0; // slot 20
|
virtual void setTextureTransformMatrixIdentity(std::uint32_t stage) = 0; // slot 20
|
||||||
virtual void setTextureTransformMatrix(std::uint32_t stage, const TextureTransform& transform) = 0;
|
virtual void setTextureTransformMatrix(std::uint32_t stage, const TextureTransform& transform) = 0;
|
||||||
virtual void setEnvironmentAmbient(std::uint32_t color) = 0;
|
virtual void setEnvironmentAmbient(Color color) = 0; // zero alpha
|
||||||
virtual PiRenderTexture* newRenderTexture(Pixmap* pixmap, std::uint32_t flags) = 0;
|
virtual PiRenderTexture* newRenderTexture(const Pixmap& pixmap, std::uint32_t flags) = 0;
|
||||||
virtual PiInternalIndexBuffer* newIndexBuffer(std::uint32_t count, const std::uint16_t* indices) = 0;
|
virtual PiInternalIndexBuffer* newIndexBuffer(std::uint32_t count, const std::uint16_t* indices) = 0;
|
||||||
virtual PiInternalVertexBuffer* newStaticVertexBuffer(const void* data, std::uint32_t a, std::uint32_t b,
|
virtual PiInternalVertexBuffer* newStaticVertexBuffer(const void* data, std::uint32_t stride,
|
||||||
std::uint32_t c) = 0; // slot 25
|
std::uint32_t elementSize,
|
||||||
virtual PiInternalVertexBuffer* newDynamicVertexBuffer(std::uint32_t a, std::uint32_t b) = 0;
|
std::uint32_t elementCount) = 0; // slot 25
|
||||||
|
// Filled later with PiInternalVertexBuffer::update.
|
||||||
|
virtual PiInternalVertexBuffer* newDynamicVertexBuffer(std::uint32_t elementSize, std::uint32_t elementCount) = 0;
|
||||||
virtual SharedPtr<PiPixmapRenderTarget> newPixmapRenderTarget(Vec2i size) = 0;
|
virtual SharedPtr<PiPixmapRenderTarget> newPixmapRenderTarget(Vec2i size) = 0;
|
||||||
virtual SharedPtr<PiTextureRenderTarget> newTextureRenderTarget(Vec2i size) = 0;
|
virtual SharedPtr<PiTextureRenderTarget> newTextureRenderTarget(Vec2i size) = 0;
|
||||||
virtual void beginRendering(PiRenderTarget* target) = 0;
|
virtual void beginRendering(PiRenderTarget* target) = 0;
|
||||||
virtual void endRendering() = 0; // slot 30
|
virtual void endRendering() = 0; // slot 30
|
||||||
|
// Vertex buffers: positions (Vec3f), normals (Vec3f), texture coordinates (Vec2f), colors (Color); all but
|
||||||
|
// positions may be null.
|
||||||
virtual void drawTriangles(PiInternalIndexBuffer* indices, PiInternalVertexBuffer* positions,
|
virtual void drawTriangles(PiInternalIndexBuffer* indices, PiInternalVertexBuffer* positions,
|
||||||
PiInternalVertexBuffer* normals, PiInternalVertexBuffer* texCoords0,
|
PiInternalVertexBuffer* normals, PiInternalVertexBuffer* texCoords,
|
||||||
PiInternalVertexBuffer* texCoords1) = 0;
|
PiInternalVertexBuffer* colors) = 0;
|
||||||
virtual void drawLine(Vec3f from, Vec3f to, std::uint32_t color) = 0;
|
virtual void drawLine(Vec3f from, Vec3f to, std::uint32_t color) = 0;
|
||||||
virtual void drawPointSprites(const PointSpriteVector& sprites) = 0;
|
virtual void drawPointSprites(const PointSpriteVector& sprites) = 0;
|
||||||
virtual void drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount,
|
virtual void drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount,
|
||||||
const std::uint16_t* indices, const VertexAttributeArray<Vec3f>& positions,
|
const std::uint16_t* indices, const VertexAttributeArray<Vec3f>& positions,
|
||||||
const VertexAttributeArray<Vec3f>& normals,
|
const VertexAttributeArray<Vec3f>& normals,
|
||||||
const VertexAttributeArray<Vec2f>& texCoords,
|
const VertexAttributeArray<Vec2f>& texCoords,
|
||||||
const VertexAttributeArray<std::uint32_t>& colors) = 0; // slot 34
|
const VertexAttributeArray<Color>& colors) = 0; // slot 34
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// PiRenderEngine's own fields, after the vtable pointer: a magic number IMVU's destructor checks, and a size
|
// PiRenderEngine's own fields, after the vtable pointer: a magic number IMVU's destructor checks, and a size
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
// The objects a PiRenderEngine creates and hands to IMVU's code: buffers, textures, render targets. IMVU's code keeps
|
||||||
|
// them, calls their virtual methods, and deletes them through their virtual destructors. Declared from the vtables in
|
||||||
|
// SceneWindow.dll; method names are ours, from what IMVU's implementations do. Not yet verified by a renderer of our
|
||||||
|
// own.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "t5de/imvu/types.hpp"
|
||||||
|
|
||||||
|
namespace t5de::imvu {
|
||||||
|
|
||||||
|
class PiInternalIndexBuffer {
|
||||||
|
public:
|
||||||
|
virtual ~PiInternalIndexBuffer() = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PiInternalVertexBuffer {
|
||||||
|
public:
|
||||||
|
virtual ~PiInternalVertexBuffer() = default;
|
||||||
|
// Replaces a dynamic buffer's contents: elementCount elements of elementSize bytes, stride bytes apart (a multiple
|
||||||
|
// of 4, at least elementSize). Element size and count are the ones the buffer was created with.
|
||||||
|
virtual void update(const void* data, std::uint32_t stride, std::uint32_t elementSize,
|
||||||
|
std::uint32_t elementCount) = 0;
|
||||||
|
// Unknown: IMVU's software buffer forgets its data; its Direct3D buffer asserts, so it isn't called there.
|
||||||
|
virtual void unknown2() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PiRenderTexture {
|
||||||
|
public:
|
||||||
|
virtual ~PiRenderTexture() = default;
|
||||||
|
virtual Vec2i size() const = 0;
|
||||||
|
// Copies the parts of pixmap in regions into the texture. (Not overloaded with update: MSVC orders overloaded
|
||||||
|
// virtual methods unlike their declaration.)
|
||||||
|
virtual void updateRegions(const Pixmap& pixmap, const CompoundRegion& regions) = 0;
|
||||||
|
// Copies all of pixmap into the texture.
|
||||||
|
virtual void update(const Pixmap& pixmap) = 0;
|
||||||
|
// Reads the texture into a new Pixmap, constructed in result (a Pixmap returned by value).
|
||||||
|
virtual Pixmap* read(Pixmap* result) const = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PiRenderTarget;
|
||||||
|
class PiPixmapRenderTarget;
|
||||||
|
class PiTextureRenderTarget;
|
||||||
|
|
||||||
|
} // namespace t5de::imvu
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
#pragma once
|
||||||
|
// The data types IMVU's renderer interface passes, as SceneWindow.dll's Direct3D 9 engine reads them. Most are passed
|
||||||
|
// by reference: where IMVU's type might hold more than we read, its size isn't asserted. See docs/interface.md.
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
struct HWND__; // windows.h's window handle type (HWND is HWND__*)
|
||||||
|
|
||||||
|
namespace t5de::imvu {
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
struct Vec2 {
|
||||||
|
T x;
|
||||||
|
T y;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
struct Vec3 {
|
||||||
|
T x;
|
||||||
|
T y;
|
||||||
|
T z;
|
||||||
|
};
|
||||||
|
|
||||||
|
using Vec2i = Vec2<int>;
|
||||||
|
using Vec2f = Vec2<float>;
|
||||||
|
using Vec3f = Vec3<float>;
|
||||||
|
|
||||||
|
// Colors are 0xAARRGGBB (Direct3D's D3DCOLOR). Light and ambient colors must have zero alpha.
|
||||||
|
using Color = std::uint32_t;
|
||||||
|
|
||||||
|
// A 4x4 matrix in Direct3D's convention: row-major, transforming row vectors (p' = p * M).
|
||||||
|
struct Mat4f {
|
||||||
|
float m[4][4];
|
||||||
|
};
|
||||||
|
|
||||||
|
// An affine transform: a 3x3 matrix (row-major), then a translation; p' = p * linear + translation, as in Direct3D.
|
||||||
|
// The Direct3D engine expands it to [linear 0; translation 1]. (What it reads; IMVU's type may be larger.)
|
||||||
|
struct Transform {
|
||||||
|
float linear[3][3];
|
||||||
|
Vec3f translation;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A texture coordinate transform: u' = u * m[0] + v * m[1] + m[4], v' = u * m[2] + v * m[3] + m[5].
|
||||||
|
struct TextureTransform {
|
||||||
|
float m[6];
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class LightType : int {
|
||||||
|
Point = 0, // at `vector`; the Direct3D engine sets no attenuation and a fixed range
|
||||||
|
Directional = 1, // shining along -`vector` (a zero vector means along -x)
|
||||||
|
};
|
||||||
|
|
||||||
|
// What the Direct3D engine reads of a light. (IMVU's type may be larger.)
|
||||||
|
struct LightProperties {
|
||||||
|
bool enabled;
|
||||||
|
Color diffuse; // zero alpha
|
||||||
|
Vec3f vector;
|
||||||
|
LightType type;
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(offsetof(LightProperties, diffuse) == 4 && offsetof(LightProperties, vector) == 8
|
||||||
|
&& offsetof(LightProperties, type) == 0x14);
|
||||||
|
|
||||||
|
// Blend functions, as source and destination factors.
|
||||||
|
enum class RenderBlendMode : int {
|
||||||
|
Alpha = 1, // source alpha, inverse source alpha
|
||||||
|
Additive = 2, // one, one
|
||||||
|
Premultiplied = 3, // one, inverse source alpha
|
||||||
|
AdditiveAlpha = 4, // source alpha, one
|
||||||
|
};
|
||||||
|
|
||||||
|
// clear()'s flags.
|
||||||
|
enum ClearFlags : std::uint32_t {
|
||||||
|
ClearColor = 1,
|
||||||
|
ClearDepth = 2, // to 1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vertex attributes in memory: count elements, stride bytes apart (0: packed).
|
||||||
|
template <class T>
|
||||||
|
struct VertexAttributeArray {
|
||||||
|
const T* data;
|
||||||
|
std::uint32_t count;
|
||||||
|
std::uint32_t stride;
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(VertexAttributeArray<Vec3f>) == 12);
|
||||||
|
|
||||||
|
// IMVU's image class (pixmap.dll): 32-bit pixels (Color). Its layout isn't needed: pixmap.dll exports its methods
|
||||||
|
// (width, height, pitch, rowBytes, pixels, ...).
|
||||||
|
class Pixmap;
|
||||||
|
|
||||||
|
// Unknown so far: used only by drawPointSprites (particles), as a Visual C++ 2010 std::vector<PointSprite>.
|
||||||
|
struct PointSpriteVector;
|
||||||
|
|
||||||
|
// Region lists IMVU passes to texture updates; unknown so far.
|
||||||
|
class CompoundRegion;
|
||||||
|
|
||||||
|
struct UiEvent;
|
||||||
|
using NativeWindowHandle = ::HWND__*; // an HWND
|
||||||
|
|
||||||
|
// IMVU's window (SceneWindow.dll's NativeWindow), as far as a renderer needs it: the first methods of its vtable.
|
||||||
|
// Its later methods include overloads, which MSVC lays out unlike their declaration: declare them from the vtable
|
||||||
|
// if they're ever needed.
|
||||||
|
class INativeWindow {
|
||||||
|
public:
|
||||||
|
virtual ~INativeWindow() = default;
|
||||||
|
virtual void handleMessage(UiEvent* event) = 0;
|
||||||
|
virtual int onSetCursor(Vec2i position) = 0;
|
||||||
|
virtual NativeWindowHandle getPlatformWindow() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace t5de::imvu
|
||||||
@@ -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