Compare commits
4
Commits
3c28a8794b
...
5c35c8b63a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c35c8b63a | ||
|
|
9a7d7d2908 | ||
|
|
0d126574bf | ||
|
|
83e0665681 |
No files matched your search
@@ -1,7 +1,9 @@
|
||||
# t5de-renderer
|
||||
|
||||
A renderer plugin for IMVU Classic (see README.md and docs/interface.md). Part of T5DE; the patches and the patcher
|
||||
are in the `t5de` repository (git.t5de.dev/t5de/t5de, usually checked out beside this one).
|
||||
A new renderer for IMVU Classic: a 32-bit adapter DLL the client loads as a renderer plugin, and a 64-bit renderer
|
||||
process drawing with Diligent Engine (see README.md, docs/architecture.md, docs/interface.md). Part of T5DE; the
|
||||
patches and the patcher are in the `t5de` repository (git.t5de.dev/t5de/t5de, usually checked out beside this one).
|
||||
The user chose this architecture for ray tracing, mesh shaders and a possible browser client later.
|
||||
|
||||
## Rules
|
||||
|
||||
@@ -13,9 +15,12 @@ are in the `t5de` repository (git.t5de.dev/t5de/t5de, usually checked out beside
|
||||
|
||||
## Notes
|
||||
|
||||
- The plugin must match IMVU's ABI: 32-bit, MSVC (cl or clang-cl; MinGW's C++ ABI differs), `__thiscall`
|
||||
virtual methods in IMVU's vtable order, Boost 1.61's `shared_ptr` layout. Exceptions must not cross into IMVU's
|
||||
code (a different C runtime); the CRT is linked statically.
|
||||
- The adapter must match IMVU's ABI: 32-bit, MSVC (cl or clang-cl; MinGW's C++ ABI differs), `__thiscall`
|
||||
virtual methods in IMVU's vtable order (never overloaded: MSVC orders overloads unlike their declaration), Boost
|
||||
1.61's `shared_ptr` layout. Exceptions must not cross into IMVU's code (a different C runtime); the CRT is linked
|
||||
statically. Code shared with the 64-bit renderer process (`include/t5de/protocol/`) uses fixed-size fields only.
|
||||
- The development VM has no graphics driver (Microsoft Basic Render Driver): Direct3D 11 works in software, IMVU's
|
||||
OpenGL renderer doesn't, and performance there means nothing.
|
||||
- Ghidra 12.2 (Java 25) is at `%LOCALAPPDATA%\Programs\ghidra_12.2_PUBLIC`; projects in
|
||||
`%LOCALAPPDATA%\t5de-dev\ghidra`, analysed headless (`support\analyzeHeadless.bat`). The client to analyse is the
|
||||
`t5de` checkout's `.cache/<version>/install`.
|
||||
+28
-25
@@ -5,15 +5,18 @@ project(t5de-renderer
|
||||
DESCRIPTION "A new 3D renderer for the IMVU Classic client"
|
||||
LANGUAGES CXX)
|
||||
|
||||
# The plugin is loaded into IMVU Classic, a 32-bit program built with Visual C++: it must use the same ABI.
|
||||
# Two programs, for two architectures (a preset for each, see CMakePresets.json):
|
||||
# - 32-bit: the adapter, t5deRenderEngine.dll, a renderer plugin loaded into IMVU Classic (a 32-bit Visual C++
|
||||
# program, so it must match its ABI).
|
||||
# - 64-bit: the renderer process, t5de-renderer.exe, which draws.
|
||||
if(NOT MSVC) # cl or clang-cl
|
||||
message(FATAL_ERROR "Build with MSVC or clang-cl: the plugin must match IMVU Classic's Visual C++ ABI.")
|
||||
endif()
|
||||
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4)
|
||||
message(FATAL_ERROR "Build for 32-bit x86 (IMVU Classic is 32-bit): use a CMakePresets.json preset.")
|
||||
message(FATAL_ERROR "Build with MSVC or clang-cl: the adapter must match IMVU Classic's Visual C++ ABI.")
|
||||
endif()
|
||||
|
||||
# The C runtime is linked statically, so the plugin needs nothing installed beside it.
|
||||
# No C++ modules: skip scanning for them.
|
||||
set(CMAKE_CXX_SCAN_FOR_MODULES OFF)
|
||||
|
||||
# The C runtime is linked statically, so the programs need nothing installed beside them.
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||
|
||||
# Warnings and conformance for our own targets.
|
||||
@@ -23,25 +26,25 @@ target_compile_features(t5de_options INTERFACE cxx_std_20)
|
||||
target_compile_options(t5de_options INTERFACE /W4 /permissive- /utf-8 /Zc:__cplusplus)
|
||||
target_compile_definitions(t5de_options INTERFACE WIN32_LEAN_AND_MEAN NOMINMAX UNICODE _UNICODE)
|
||||
|
||||
# Our declarations of IMVU's interfaces.
|
||||
add_library(t5de_imvu INTERFACE)
|
||||
add_library(t5de::imvu ALIAS t5de_imvu)
|
||||
target_include_directories(t5de_imvu INTERFACE
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
|
||||
target_compile_features(t5de_imvu INTERFACE cxx_std_20)
|
||||
|
||||
# The renderer plugin, t5deRenderEngine.dll.
|
||||
add_library(t5de_render_engine SHARED
|
||||
src/log.cpp
|
||||
src/passthrough_engine.cpp
|
||||
src/plugin.cpp
|
||||
src/plugin.def)
|
||||
set_target_properties(t5de_render_engine PROPERTIES OUTPUT_NAME t5deRenderEngine)
|
||||
target_link_libraries(t5de_render_engine PRIVATE t5de::imvu t5de::options)
|
||||
|
||||
install(TARGETS t5de_render_engine RUNTIME DESTINATION .)
|
||||
# What the adapter and the renderer process share.
|
||||
add_library(t5de_protocol INTERFACE)
|
||||
add_library(t5de::protocol ALIAS t5de_protocol)
|
||||
target_include_directories(t5de_protocol INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
|
||||
target_compile_features(t5de_protocol INTERFACE cxx_std_20)
|
||||
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(tests)
|
||||
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 4)
|
||||
# Our declarations of IMVU's interfaces (32-bit layouts).
|
||||
add_library(t5de_imvu INTERFACE)
|
||||
add_library(t5de::imvu ALIAS t5de_imvu)
|
||||
target_include_directories(t5de_imvu INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
|
||||
target_compile_features(t5de_imvu INTERFACE cxx_std_20)
|
||||
|
||||
add_subdirectory(adapter)
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
else()
|
||||
add_subdirectory(renderer)
|
||||
endif()
|
||||
+33
-12
@@ -3,29 +3,43 @@
|
||||
"cmakeMinimumRequired": { "major": 3, "minor": 28, "patch": 0 },
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "windows-x86",
|
||||
"displayName": "Windows, 32-bit (Visual Studio Build Tools)",
|
||||
"description": "IMVU Classic is a 32-bit Visual C++ program: build for Win32 with MSVC.",
|
||||
"name": "windows",
|
||||
"hidden": true,
|
||||
"generator": "Visual Studio 18 2026",
|
||||
"architecture": { "value": "Win32", "strategy": "set" },
|
||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||
"installDir": "${sourceDir}/build/${presetName}/install",
|
||||
"installDir": "${sourceDir}/build/install",
|
||||
"condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }
|
||||
},
|
||||
{
|
||||
"name": "windows-x86",
|
||||
"inherits": "windows",
|
||||
"displayName": "Adapter (32-bit)",
|
||||
"description": "t5deRenderEngine.dll, loaded into IMVU Classic, a 32-bit Visual C++ program.",
|
||||
"architecture": { "value": "Win32", "strategy": "set" }
|
||||
},
|
||||
{
|
||||
"name": "windows-x64",
|
||||
"inherits": "windows",
|
||||
"displayName": "Renderer process (64-bit)",
|
||||
"description": "t5de-renderer.exe, with Diligent Engine (fetched and built on the first configure).",
|
||||
"architecture": { "value": "x64", "strategy": "set" }
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{ "name": "debug", "configurePreset": "windows-x86", "configuration": "Debug" },
|
||||
{ "name": "release", "configurePreset": "windows-x86", "configuration": "Release" }
|
||||
{ "name": "adapter-debug", "configurePreset": "windows-x86", "configuration": "Debug" },
|
||||
{ "name": "adapter-release", "configurePreset": "windows-x86", "configuration": "Release" },
|
||||
{ "name": "renderer-debug", "configurePreset": "windows-x64", "configuration": "Debug" },
|
||||
{ "name": "renderer-release", "configurePreset": "windows-x64", "configuration": "Release" }
|
||||
],
|
||||
"testPresets": [
|
||||
{
|
||||
"name": "debug",
|
||||
"name": "adapter-debug",
|
||||
"configurePreset": "windows-x86",
|
||||
"configuration": "Debug",
|
||||
"output": { "outputOnFailure": true }
|
||||
},
|
||||
{
|
||||
"name": "release",
|
||||
"name": "adapter-release",
|
||||
"configurePreset": "windows-x86",
|
||||
"configuration": "Release",
|
||||
"output": { "outputOnFailure": true }
|
||||
@@ -33,11 +47,18 @@
|
||||
],
|
||||
"workflowPresets": [
|
||||
{
|
||||
"name": "release",
|
||||
"name": "adapter",
|
||||
"steps": [
|
||||
{ "type": "configure", "name": "windows-x86" },
|
||||
{ "type": "build", "name": "release" },
|
||||
{ "type": "test", "name": "release" }
|
||||
{ "type": "build", "name": "adapter-release" },
|
||||
{ "type": "test", "name": "adapter-release" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "renderer",
|
||||
"steps": [
|
||||
{ "type": "configure", "name": "windows-x64" },
|
||||
{ "type": "build", "name": "renderer-release" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
# t5de-renderer
|
||||
|
||||
A new 3D renderer for the IMVU Classic client, as a renderer plugin the client loads like its own OpenGL renderer.
|
||||
Part of [T5DE](https://git.t5de.dev/t5de/t5de); unofficial, not affiliated with IMVU.
|
||||
A new 3D renderer for the IMVU Classic client. Part of [T5DE](https://git.t5de.dev/t5de/t5de); unofficial, not
|
||||
affiliated with IMVU.
|
||||
|
||||
For now it's a pass-through: it wraps IMVU's Direct3D renderer and logs what each frame draws, to prove our
|
||||
declarations of IMVU's interface (`docs/interface.md`).
|
||||
Two programs (`docs/architecture.md`): an adapter, `t5deRenderEngine.dll`, which IMVU Classic loads as a renderer
|
||||
plugin, and a 64-bit renderer process, `t5de-renderer.exe`, which draws with
|
||||
[Diligent Engine](https://github.com/DiligentGraphics/DiligentCore). For now the client's own Direct3D renderer still
|
||||
draws the scene, and the renderer process proves the setup by drawing a small box over it. `docs/interface.md` has
|
||||
what we know of IMVU's renderer interface.
|
||||
|
||||
## Building
|
||||
|
||||
Needs Visual Studio Build Tools 2026 (the C++ workload) and CMake 3.28 or newer. IMVU Classic is a 32-bit
|
||||
Visual C++ program, so the plugin is built for 32-bit Windows (the `windows-x86` preset):
|
||||
Needs Visual Studio Build Tools 2026 (the C++ workload) and CMake 3.28 or newer. One preset per program:
|
||||
|
||||
```sh
|
||||
cmake --workflow --preset release
|
||||
cmake --workflow --preset adapter # build/windows-x86/adapter/Release/t5deRenderEngine.dll, and the tests
|
||||
cmake --workflow --preset renderer # build/windows-x64/renderer/Release/t5de-renderer.exe
|
||||
```
|
||||
|
||||
That configures, builds `build/windows-x86/Release/t5deRenderEngine.dll` and runs the tests. Or step by step:
|
||||
`cmake --preset windows-x86`, `cmake --build --preset debug`, `ctest --preset debug`.
|
||||
The renderer's first configure fetches Diligent Engine (pinned in `renderer/CMakeLists.txt`) and builds it, which
|
||||
takes a while.
|
||||
|
||||
## Trying it
|
||||
|
||||
Copy `t5deRenderEngine.dll` into a T5DE client folder built with the `t5de-renderer` patch, which has the OpenGL
|
||||
setting load it in place of `glRenderEngine.dll`, then choose OpenGL in Settings > Advanced. The log is
|
||||
Copy both programs into a T5DE client folder built with the `t5de-renderer` patch, which has the OpenGL setting load
|
||||
the adapter in place of `glRenderEngine.dll`, then choose OpenGL in Settings > Advanced. The adapter's log is
|
||||
`%TEMP%\t5de-renderer.log`.
|
||||
@@ -0,0 +1,12 @@
|
||||
# t5deRenderEngine.dll, the adapter: a renderer plugin for IMVU Classic (32-bit).
|
||||
|
||||
add_library(t5de_render_engine SHARED
|
||||
log.cpp
|
||||
passthrough_engine.cpp
|
||||
plugin.cpp
|
||||
plugin.def
|
||||
renderer_process.cpp)
|
||||
set_target_properties(t5de_render_engine PROPERTIES OUTPUT_NAME t5deRenderEngine)
|
||||
target_link_libraries(t5de_render_engine PRIVATE t5de::imvu t5de::protocol t5de::options)
|
||||
|
||||
install(TARGETS t5de_render_engine RUNTIME DESTINATION .)
|
||||
File renamed without changes.
File renamed without changes.
@@ -1,5 +1,8 @@
|
||||
#include "passthrough_engine.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
|
||||
#include "log.hpp"
|
||||
@@ -49,9 +52,50 @@ constexpr std::array<const char*, PassthroughEngine::slots> names = {
|
||||
// Frames whose calls are logged: the first few, then one a minute or so at 30 FPS.
|
||||
bool logged(std::uint64_t frame) { return frame < 5 || frame % 1800 == 0; }
|
||||
|
||||
// Whether to log a call's arguments: the first few calls of each method, in the whole process. Enough to check that
|
||||
// our layouts of IMVU's types read sensible values.
|
||||
bool sampled(std::size_t slot)
|
||||
{
|
||||
static std::array<std::atomic<int>, PassthroughEngine::slots> calls{};
|
||||
return calls[slot]++ < 3;
|
||||
}
|
||||
|
||||
std::string text(const imvu::Vec3f& v) { return std::format("({:.3f}, {:.3f}, {:.3f})", v.x, v.y, v.z); }
|
||||
|
||||
std::string text(const imvu::Transform& t)
|
||||
{
|
||||
const auto& m = t.linear;
|
||||
return std::format("[{:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f}] + {}", m[0][0], m[0][1],
|
||||
m[0][2], m[1][0], m[1][1], m[1][2], m[2][0], m[2][1], m[2][2], text(t.translation));
|
||||
}
|
||||
|
||||
// A pixmap's size, from pixmap.dll's exported Pixmap::width and Pixmap::height.
|
||||
imvu::Vec2i pixmap_size(const imvu::Pixmap& pixmap)
|
||||
{
|
||||
using Getter = unsigned(__thiscall*)(const imvu::Pixmap*);
|
||||
struct Getters {
|
||||
Getter width;
|
||||
Getter height;
|
||||
};
|
||||
static const Getters getters = [] {
|
||||
HMODULE module = GetModuleHandleW(L"pixmap.dll");
|
||||
if (module == nullptr) {
|
||||
return Getters{};
|
||||
}
|
||||
return Getters{reinterpret_cast<Getter>(GetProcAddress(module, "?width@Pixmap@@QBEIXZ")),
|
||||
reinterpret_cast<Getter>(GetProcAddress(module, "?height@Pixmap@@QBEIXZ"))};
|
||||
}();
|
||||
if (getters.width == nullptr || getters.height == nullptr) {
|
||||
return {-1, -1};
|
||||
}
|
||||
return {static_cast<int>(getters.width(&pixmap)), static_cast<int>(getters.height(&pixmap))};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PassthroughEngine::PassthroughEngine(imvu::SharedPtr<imvu::PiRenderEngine> inner) : inner_(std::move(inner))
|
||||
PassthroughEngine::PassthroughEngine(imvu::SharedPtr<imvu::PiRenderEngine> inner,
|
||||
std::unique_ptr<RendererProcess> renderer)
|
||||
: inner_(std::move(inner)), renderer_(std::move(renderer))
|
||||
{
|
||||
log::info("passthrough: wrapping the engine at {}", static_cast<void*>(inner_.get()));
|
||||
}
|
||||
@@ -73,26 +117,42 @@ void PassthroughEngine::enable2dFrameBuffer()
|
||||
inner_->enable2dFrameBuffer();
|
||||
}
|
||||
|
||||
void PassthroughEngine::clear(std::uint32_t flags, std::uint32_t color)
|
||||
void PassthroughEngine::clear(std::uint32_t flags, imvu::Color color)
|
||||
{
|
||||
if (sampled(3)) {
|
||||
log::info("clear(flags {}, color {:08x})", flags, color);
|
||||
}
|
||||
count(3);
|
||||
inner_->clear(flags, color);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection)
|
||||
{
|
||||
if (sampled(4)) {
|
||||
const auto& m = projection.m;
|
||||
log::info("setProjection({}x{}, [{:.3f} {:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f} {:.3f}; "
|
||||
"{:.3f} {:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f} {:.3f}])",
|
||||
viewport.x, viewport.y, m[0][0], m[0][1], m[0][2], m[0][3], m[1][0], m[1][1], m[1][2],
|
||||
m[1][3], m[2][0], m[2][1], m[2][2], m[2][3], m[3][0], m[3][1], m[3][2], m[3][3]);
|
||||
}
|
||||
count(4);
|
||||
inner_->setProjection(viewport, projection);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setModelSpace(const imvu::Transform& model)
|
||||
{
|
||||
if (sampled(5)) {
|
||||
log::info("setModelSpace({})", text(model));
|
||||
}
|
||||
count(5);
|
||||
inner_->setModelSpace(model);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setViewSpace(const imvu::Transform& view)
|
||||
{
|
||||
if (sampled(6)) {
|
||||
log::info("setViewSpace({})", text(view));
|
||||
}
|
||||
count(6);
|
||||
inner_->setViewSpace(view);
|
||||
}
|
||||
@@ -117,6 +177,9 @@ void PassthroughEngine::setDepthWritingOn(bool on)
|
||||
|
||||
void PassthroughEngine::setBlendMode(imvu::RenderBlendMode mode)
|
||||
{
|
||||
if (sampled(10)) {
|
||||
log::info("setBlendMode({})", static_cast<int>(mode));
|
||||
}
|
||||
count(10);
|
||||
inner_->setBlendMode(mode);
|
||||
}
|
||||
@@ -129,6 +192,9 @@ void PassthroughEngine::setAlphaTestingOn(bool on)
|
||||
|
||||
void PassthroughEngine::setAlphaTestValue(float value)
|
||||
{
|
||||
if (sampled(12)) {
|
||||
log::info("setAlphaTestValue({:.3f})", value);
|
||||
}
|
||||
count(12);
|
||||
inner_->setAlphaTestValue(value);
|
||||
}
|
||||
@@ -159,6 +225,10 @@ void PassthroughEngine::setLightingOn(bool on)
|
||||
|
||||
void PassthroughEngine::setLightProperties(std::uint32_t index, const imvu::LightProperties& light)
|
||||
{
|
||||
if (sampled(17)) {
|
||||
log::info("setLightProperties({}, enabled {}, diffuse {:08x}, vector {}, type {})", index,
|
||||
light.enabled, light.diffuse, text(light.vector), static_cast<int>(light.type));
|
||||
}
|
||||
count(17);
|
||||
inner_->setLightProperties(index, light);
|
||||
}
|
||||
@@ -169,8 +239,11 @@ void PassthroughEngine::setFogOn(bool on)
|
||||
inner_->setFogOn(on);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setFogAttributes(float start, float end, std::uint32_t color)
|
||||
void PassthroughEngine::setFogAttributes(float start, float end, imvu::Color color)
|
||||
{
|
||||
if (sampled(19)) {
|
||||
log::info("setFogAttributes(start {:.3f}, end {:.3f}, color {:08x})", start, end, color);
|
||||
}
|
||||
count(19);
|
||||
inner_->setFogAttributes(start, end, color);
|
||||
}
|
||||
@@ -183,39 +256,61 @@ void PassthroughEngine::setTextureTransformMatrixIdentity(std::uint32_t stage)
|
||||
|
||||
void PassthroughEngine::setTextureTransformMatrix(std::uint32_t stage, const imvu::TextureTransform& transform)
|
||||
{
|
||||
if (sampled(21)) {
|
||||
const auto& m = transform.m;
|
||||
log::info("setTextureTransformMatrix({}, [{:.3f} {:.3f} {:.3f} {:.3f} {:.3f} {:.3f}])", stage, m[0],
|
||||
m[1], m[2], m[3], m[4], m[5]);
|
||||
}
|
||||
count(21);
|
||||
inner_->setTextureTransformMatrix(stage, transform);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setEnvironmentAmbient(std::uint32_t color)
|
||||
void PassthroughEngine::setEnvironmentAmbient(imvu::Color color)
|
||||
{
|
||||
if (sampled(22)) {
|
||||
log::info("setEnvironmentAmbient({:08x})", color);
|
||||
}
|
||||
count(22);
|
||||
inner_->setEnvironmentAmbient(color);
|
||||
}
|
||||
|
||||
imvu::PiRenderTexture* PassthroughEngine::newRenderTexture(imvu::Pixmap* pixmap, std::uint32_t flags)
|
||||
imvu::PiRenderTexture* PassthroughEngine::newRenderTexture(const imvu::Pixmap& pixmap, std::uint32_t flags)
|
||||
{
|
||||
if (sampled(23)) {
|
||||
const auto size = pixmap_size(pixmap);
|
||||
log::info("newRenderTexture({}x{} pixmap, flags {:x})", size.x, size.y, flags);
|
||||
}
|
||||
count(23);
|
||||
return inner_->newRenderTexture(pixmap, flags);
|
||||
}
|
||||
|
||||
imvu::PiInternalIndexBuffer* PassthroughEngine::newIndexBuffer(std::uint32_t indexCount, const std::uint16_t* indices)
|
||||
{
|
||||
if (sampled(24)) {
|
||||
log::info("newIndexBuffer({} indices)", indexCount);
|
||||
}
|
||||
count(24);
|
||||
return inner_->newIndexBuffer(indexCount, indices);
|
||||
}
|
||||
|
||||
imvu::PiInternalVertexBuffer* PassthroughEngine::newStaticVertexBuffer(const void* data, std::uint32_t a,
|
||||
std::uint32_t b, std::uint32_t c)
|
||||
imvu::PiInternalVertexBuffer* PassthroughEngine::newStaticVertexBuffer(const void* data, std::uint32_t stride,
|
||||
std::uint32_t elementSize, std::uint32_t elementCount)
|
||||
{
|
||||
if (sampled(25)) {
|
||||
log::info("newStaticVertexBuffer(stride {}, element size {}, {} elements)", stride, elementSize,
|
||||
elementCount);
|
||||
}
|
||||
count(25);
|
||||
return inner_->newStaticVertexBuffer(data, a, b, c);
|
||||
return inner_->newStaticVertexBuffer(data, stride, elementSize, elementCount);
|
||||
}
|
||||
|
||||
imvu::PiInternalVertexBuffer* PassthroughEngine::newDynamicVertexBuffer(std::uint32_t a, std::uint32_t b)
|
||||
imvu::PiInternalVertexBuffer* PassthroughEngine::newDynamicVertexBuffer(std::uint32_t elementSize, std::uint32_t elementCount)
|
||||
{
|
||||
if (sampled(26)) {
|
||||
log::info("newDynamicVertexBuffer(element size {}, {} elements)", elementSize, elementCount);
|
||||
}
|
||||
count(26);
|
||||
return inner_->newDynamicVertexBuffer(a, b);
|
||||
return inner_->newDynamicVertexBuffer(elementSize, elementCount);
|
||||
}
|
||||
|
||||
imvu::SharedPtr<imvu::PiPixmapRenderTarget> PassthroughEngine::newPixmapRenderTarget(imvu::Vec2i size)
|
||||
@@ -242,6 +337,9 @@ void PassthroughEngine::endRendering()
|
||||
{
|
||||
count(30);
|
||||
inner_->endRendering();
|
||||
if (renderer_) {
|
||||
renderer_->frameDrawn();
|
||||
}
|
||||
if (logged(frames_)) {
|
||||
std::string line = std::format("frame {}:", frames_);
|
||||
for (std::size_t slot = 1; slot < slots; ++slot) {
|
||||
@@ -257,15 +355,18 @@ void PassthroughEngine::endRendering()
|
||||
}
|
||||
|
||||
void PassthroughEngine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
|
||||
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords0,
|
||||
imvu::PiInternalVertexBuffer* texCoords1)
|
||||
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
|
||||
imvu::PiInternalVertexBuffer* colors)
|
||||
{
|
||||
count(31);
|
||||
inner_->drawTriangles(indices, positions, normals, texCoords0, texCoords1);
|
||||
inner_->drawTriangles(indices, positions, normals, texCoords, colors);
|
||||
}
|
||||
|
||||
void PassthroughEngine::drawLine(imvu::Vec3f from, imvu::Vec3f to, std::uint32_t color)
|
||||
{
|
||||
if (sampled(32)) {
|
||||
log::info("drawLine({}, {}, {:08x})", text(from), text(to), color);
|
||||
}
|
||||
count(32);
|
||||
inner_->drawLine(from, to, color);
|
||||
}
|
||||
@@ -281,8 +382,12 @@ void PassthroughEngine::drawTrianglesFromMemory(std::uint32_t vertexCount, std::
|
||||
const imvu::VertexAttributeArray<imvu::Vec3f>& positions,
|
||||
const imvu::VertexAttributeArray<imvu::Vec3f>& normals,
|
||||
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
|
||||
const imvu::VertexAttributeArray<std::uint32_t>& colors)
|
||||
const imvu::VertexAttributeArray<imvu::Color>& colors)
|
||||
{
|
||||
if (sampled(34)) {
|
||||
log::info("drawTrianglesFromMemory({} vertices, {} indices; positions {} stride {}, colors {} stride {})",
|
||||
vertexCount, indexCount, positions.count, positions.stride, colors.count, colors.stride);
|
||||
}
|
||||
count(34);
|
||||
inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, colors);
|
||||
}
|
||||
@@ -4,19 +4,22 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "renderer_process.hpp"
|
||||
#include "t5de/imvu/render_engine.hpp"
|
||||
|
||||
namespace t5de {
|
||||
|
||||
class PassthroughEngine final : public imvu::PiRenderEngine {
|
||||
public:
|
||||
explicit PassthroughEngine(imvu::SharedPtr<imvu::PiRenderEngine> inner);
|
||||
// renderer may be null.
|
||||
PassthroughEngine(imvu::SharedPtr<imvu::PiRenderEngine> inner, std::unique_ptr<RendererProcess> renderer);
|
||||
~PassthroughEngine() override;
|
||||
|
||||
void resizeFrameBuffer(imvu::Vec2i size) override;
|
||||
void enable2dFrameBuffer() override;
|
||||
void clear(std::uint32_t flags, std::uint32_t color) override;
|
||||
void clear(std::uint32_t flags, imvu::Color color) override;
|
||||
void setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection) override;
|
||||
void setModelSpace(const imvu::Transform& model) override;
|
||||
void setViewSpace(const imvu::Transform& view) override;
|
||||
@@ -32,29 +35,31 @@ public:
|
||||
void setLightingOn(bool on) override;
|
||||
void setLightProperties(std::uint32_t index, const imvu::LightProperties& light) override;
|
||||
void setFogOn(bool on) override;
|
||||
void setFogAttributes(float start, float end, std::uint32_t color) override;
|
||||
void setFogAttributes(float start, float end, imvu::Color color) override;
|
||||
void setTextureTransformMatrixIdentity(std::uint32_t stage) override;
|
||||
void setTextureTransformMatrix(std::uint32_t stage, const imvu::TextureTransform& transform) override;
|
||||
void setEnvironmentAmbient(std::uint32_t color) override;
|
||||
imvu::PiRenderTexture* newRenderTexture(imvu::Pixmap* pixmap, std::uint32_t flags) override;
|
||||
void setEnvironmentAmbient(imvu::Color color) override;
|
||||
imvu::PiRenderTexture* newRenderTexture(const imvu::Pixmap& pixmap, std::uint32_t flags) override;
|
||||
imvu::PiInternalIndexBuffer* newIndexBuffer(std::uint32_t count, const std::uint16_t* indices) override;
|
||||
imvu::PiInternalVertexBuffer* newStaticVertexBuffer(const void* data, std::uint32_t a, std::uint32_t b,
|
||||
std::uint32_t c) override;
|
||||
imvu::PiInternalVertexBuffer* newDynamicVertexBuffer(std::uint32_t a, std::uint32_t b) override;
|
||||
imvu::PiInternalVertexBuffer* newStaticVertexBuffer(const void* data, std::uint32_t stride,
|
||||
std::uint32_t elementSize,
|
||||
std::uint32_t elementCount) override;
|
||||
imvu::PiInternalVertexBuffer* newDynamicVertexBuffer(std::uint32_t elementSize,
|
||||
std::uint32_t elementCount) override;
|
||||
imvu::SharedPtr<imvu::PiPixmapRenderTarget> newPixmapRenderTarget(imvu::Vec2i size) override;
|
||||
imvu::SharedPtr<imvu::PiTextureRenderTarget> newTextureRenderTarget(imvu::Vec2i size) override;
|
||||
void beginRendering(imvu::PiRenderTarget* target) override;
|
||||
void endRendering() override;
|
||||
void drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
|
||||
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords0,
|
||||
imvu::PiInternalVertexBuffer* texCoords1) override;
|
||||
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
|
||||
imvu::PiInternalVertexBuffer* colors) override;
|
||||
void drawLine(imvu::Vec3f from, imvu::Vec3f to, std::uint32_t color) override;
|
||||
void drawPointSprites(const imvu::PointSpriteVector& sprites) override;
|
||||
void drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount, const std::uint16_t* indices,
|
||||
const imvu::VertexAttributeArray<imvu::Vec3f>& positions,
|
||||
const imvu::VertexAttributeArray<imvu::Vec3f>& normals,
|
||||
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
|
||||
const imvu::VertexAttributeArray<std::uint32_t>& colors) override;
|
||||
const imvu::VertexAttributeArray<imvu::Color>& colors) override;
|
||||
|
||||
// The interface's methods, by vtable slot (slot 0, the destructor, isn't counted).
|
||||
static constexpr std::size_t slots = 35;
|
||||
@@ -63,6 +68,7 @@ private:
|
||||
void count(std::size_t slot);
|
||||
|
||||
imvu::SharedPtr<imvu::PiRenderEngine> inner_;
|
||||
std::unique_ptr<RendererProcess> renderer_;
|
||||
std::array<std::uint32_t, slots> calls_{};
|
||||
std::uint64_t frames_ = 0;
|
||||
};
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "log.hpp"
|
||||
#include "passthrough_engine.hpp"
|
||||
#include "renderer_process.hpp"
|
||||
#include "t5de/imvu/render_engine.hpp"
|
||||
|
||||
namespace {
|
||||
@@ -39,11 +40,14 @@ extern "C" imvu::SharedPtr<imvu::PiRenderEngine> __cdecl createRenderEngine(imvu
|
||||
{
|
||||
try {
|
||||
log::info("createRenderEngine: window {}", static_cast<void*>(window.get()));
|
||||
HWND hwnd = window ? window->getPlatformWindow() : nullptr;
|
||||
auto inner = create_imvu_engine(std::move(window));
|
||||
if (!inner) {
|
||||
return {};
|
||||
}
|
||||
return imvu::SharedPtr<imvu::PiRenderEngine>(std::make_unique<PassthroughEngine>(std::move(inner)));
|
||||
auto renderer = hwnd != nullptr ? RendererProcess::start(hwnd) : nullptr;
|
||||
return imvu::SharedPtr<imvu::PiRenderEngine>(
|
||||
std::make_unique<PassthroughEngine>(std::move(inner), std::move(renderer)));
|
||||
} catch (const std::exception& e) {
|
||||
// Exceptions mustn't cross into IMVU's code (a different C++ runtime).
|
||||
log::info("createRenderEngine failed: {}", e.what());
|
||||
File renamed without changes.
@@ -0,0 +1,92 @@
|
||||
#include "renderer_process.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
#include "log.hpp"
|
||||
|
||||
namespace t5de {
|
||||
|
||||
namespace {
|
||||
|
||||
// The folder this DLL is in: the client's program folder.
|
||||
std::filesystem::path module_folder()
|
||||
{
|
||||
HMODULE self = nullptr;
|
||||
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(&module_folder), &self);
|
||||
wchar_t path[MAX_PATH];
|
||||
GetModuleFileNameW(self, path, MAX_PATH);
|
||||
return std::filesystem::path(path).parent_path();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<RendererProcess> RendererProcess::start(HWND window)
|
||||
{
|
||||
static std::atomic<int> engines = 0;
|
||||
const std::wstring name = std::format(L"Local\\t5de-renderer-{}-{}", GetCurrentProcessId(), engines++);
|
||||
|
||||
std::unique_ptr<RendererProcess> renderer(new RendererProcess());
|
||||
renderer->mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0,
|
||||
sizeof(protocol::Channel), name.c_str());
|
||||
if (renderer->mapping_ == nullptr) {
|
||||
log::info("renderer process: can't create the channel: error {}", GetLastError());
|
||||
return nullptr;
|
||||
}
|
||||
renderer->channel_ = static_cast<protocol::Channel*>(
|
||||
MapViewOfFile(renderer->mapping_, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(protocol::Channel)));
|
||||
renderer->frameEvent_ = CreateEventW(nullptr, FALSE, FALSE, (name + protocol::frameEventSuffix).c_str());
|
||||
if (renderer->channel_ == nullptr || renderer->frameEvent_ == nullptr) {
|
||||
log::info("renderer process: can't set up the channel: error {}", GetLastError());
|
||||
return nullptr;
|
||||
}
|
||||
*renderer->channel_ = {.version = protocol::version,
|
||||
.closing = 0,
|
||||
.window = reinterpret_cast<std::uintptr_t>(window),
|
||||
.frames = 0};
|
||||
|
||||
// The renderer's window goes on top of the client's 3D view: keep the client's drawing out of it.
|
||||
SetWindowLongW(window, GWL_STYLE, GetWindowLongW(window, GWL_STYLE) | WS_CLIPCHILDREN);
|
||||
|
||||
const auto exe = module_folder() / L"t5de-renderer.exe";
|
||||
std::wstring command = std::format(L"\"{}\" --channel {} --parent {}", exe.wstring(), name, GetCurrentProcessId());
|
||||
STARTUPINFOW startup{.cb = sizeof(STARTUPINFOW)};
|
||||
PROCESS_INFORMATION process{};
|
||||
if (!CreateProcessW(exe.c_str(), command.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &startup,
|
||||
&process)) {
|
||||
log::info("renderer process: can't start {}: error {}", exe.string(), GetLastError());
|
||||
return nullptr;
|
||||
}
|
||||
CloseHandle(process.hThread);
|
||||
renderer->process_ = process.hProcess;
|
||||
log::info("renderer process: started {} (pid {}) on channel {}", exe.string(), process.dwProcessId,
|
||||
std::filesystem::path(name).string());
|
||||
return renderer;
|
||||
}
|
||||
|
||||
RendererProcess::~RendererProcess()
|
||||
{
|
||||
if (channel_ != nullptr) {
|
||||
channel_->closing = 1;
|
||||
if (frameEvent_ != nullptr) {
|
||||
SetEvent(frameEvent_);
|
||||
}
|
||||
UnmapViewOfFile(channel_);
|
||||
}
|
||||
for (HANDLE handle : {frameEvent_, mapping_, process_}) {
|
||||
if (handle != nullptr) {
|
||||
CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RendererProcess::frameDrawn()
|
||||
{
|
||||
++channel_->frames;
|
||||
SetEvent(frameEvent_);
|
||||
}
|
||||
|
||||
} // namespace t5de
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
// The renderer process (t5de-renderer.exe, 64-bit) an engine draws through: started with the engine, told about
|
||||
// each frame through a shared memory channel (include/t5de/protocol/channel.hpp), and told to exit with it.
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "t5de/protocol/channel.hpp"
|
||||
|
||||
namespace t5de {
|
||||
|
||||
class RendererProcess {
|
||||
public:
|
||||
// Starts t5de-renderer.exe from this DLL's folder, drawing into window; null if it can't.
|
||||
static std::unique_ptr<RendererProcess> start(HWND window);
|
||||
|
||||
RendererProcess(const RendererProcess&) = delete;
|
||||
RendererProcess& operator=(const RendererProcess&) = delete;
|
||||
~RendererProcess();
|
||||
|
||||
// The client drew a frame.
|
||||
void frameDrawn();
|
||||
|
||||
private:
|
||||
RendererProcess() = default;
|
||||
|
||||
HANDLE mapping_ = nullptr;
|
||||
HANDLE frameEvent_ = nullptr;
|
||||
HANDLE process_ = nullptr;
|
||||
protocol::Channel* channel_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace t5de
|
||||
@@ -0,0 +1,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.
|
||||
`include/t5de/imvu/shared_ptr.hpp` matches it. With non-trivial copy and destruction, it's returned through a
|
||||
hidden pointer and passed on the stack, as IMVU's code expects.
|
||||
- Still to work out, for a renderer of our own: `Mat4f`, `Transform`, `LightProperties`, `TextureTransform`,
|
||||
`Pixmap` (`pixmap.dll`), the buffers' contents, `VertexAttributeArray<T>`, `std::vector<PointSprite>` as
|
||||
Visual C++ 2010 lays it out, and `RenderBlendMode`'s values.
|
||||
- The data types are in `include/t5de/imvu/types.hpp`, worked out from the Direct3D engine: matrices and
|
||||
transforms in Direct3D's row-vector convention, colors as `0xAARRGGBB`, lights (point or directional), blend
|
||||
modes, linear fog, texture coordinate transforms, vertex attribute arrays (data, count, stride). `Pixmap` is used
|
||||
through `pixmap.dll`'s exported methods (`width`, `height`, `pitch`, `pixels`, ...), not its layout.
|
||||
- `drawTriangles`' vertex buffers are positions (`Vec3f`), normals (`Vec3f`), texture coordinates (`Vec2f`) and
|
||||
colors; vertex buffers are made with a stride, element size and count, dynamic ones filled later through the
|
||||
buffer's own `update`. The objects an engine creates (buffers, textures, render targets) are declared in
|
||||
`include/t5de/imvu/resources.hpp`; IMVU's code deletes them through their virtual destructors.
|
||||
- Checked in the client with the pass-through's logged arguments: a right-handed perspective projection (45 degree
|
||||
vertical field of view, Direct3D's 0 to 1 depth) and an orthographic one for 2D, orthonormal view rotations,
|
||||
opaque clear colors, ambient light with zero alpha, linear fog (1 to 50), scrolling texture transforms. Furniture
|
||||
vertices are interleaved: positions and normals are 12-byte elements 32 bytes apart, so strides matter.
|
||||
- Still unknown: point sprites (`drawPointSprites`, particles), render targets, `CompoundRegion` (texture update
|
||||
regions), `newRenderTexture`'s flags, and what `beginRendering` does with a null target. MSVC orders overloaded
|
||||
virtual methods unlike their declaration, so our declarations never overload virtual methods.
|
||||
- Memory crosses the boundary only through virtual methods (destructors, control blocks), so each side can use its
|
||||
own C runtime. Exceptions must not cross it.
|
||||
|
||||
|
||||
@@ -1,54 +1,16 @@
|
||||
#pragma once
|
||||
// IMVU's renderer interface, PiRenderEngine (SceneWindow.dll), declared to match its vtable: the order of the methods
|
||||
// and their parameter types are what matter. The names are IMVU's, from SceneWindow.dll's exported RenderEngineDx
|
||||
// methods; parameter names are ours, and guesses until the calls are traced. Types used only through pointers or
|
||||
// references are left incomplete until we need their layout. See docs/interface.md.
|
||||
// methods; parameter names are ours, from what IMVU's Direct3D engine does with them. See docs/interface.md.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "t5de/imvu/resources.hpp"
|
||||
#include "t5de/imvu/shared_ptr.hpp"
|
||||
#include "t5de/imvu/types.hpp"
|
||||
|
||||
namespace t5de::imvu {
|
||||
|
||||
template <class T>
|
||||
struct Vec2 {
|
||||
T x;
|
||||
T y;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
struct Vec3 {
|
||||
T x;
|
||||
T y;
|
||||
T z;
|
||||
};
|
||||
|
||||
using Vec2i = Vec2<int>;
|
||||
using Vec2f = Vec2<float>;
|
||||
using Vec3f = Vec3<float>;
|
||||
|
||||
struct Mat4f;
|
||||
struct Transform;
|
||||
struct LightProperties;
|
||||
struct TextureTransform;
|
||||
struct PointSprite;
|
||||
class Pixmap;
|
||||
class INativeWindow;
|
||||
class PiRenderTexture;
|
||||
class PiInternalIndexBuffer;
|
||||
class PiInternalVertexBuffer;
|
||||
class PiRenderTarget;
|
||||
class PiPixmapRenderTarget;
|
||||
class PiTextureRenderTarget;
|
||||
template <class T>
|
||||
class VertexAttributeArray;
|
||||
|
||||
// std::vector<PointSprite> as Visual C++ 2010 lays it out.
|
||||
struct PointSpriteVector;
|
||||
|
||||
// Values not yet known.
|
||||
enum class RenderBlendMode : int {};
|
||||
|
||||
class PiRenderEngine {
|
||||
public:
|
||||
PiRenderEngine() = default;
|
||||
@@ -59,7 +21,7 @@ public:
|
||||
|
||||
virtual void resizeFrameBuffer(Vec2i size) = 0;
|
||||
virtual void enable2dFrameBuffer() = 0;
|
||||
virtual void clear(std::uint32_t flags, std::uint32_t color) = 0;
|
||||
virtual void clear(std::uint32_t flags, Color color) = 0; // ClearFlags
|
||||
virtual void setProjection(Vec2i viewport, const Mat4f& projection) = 0;
|
||||
virtual void setModelSpace(const Transform& model) = 0; // slot 5
|
||||
virtual void setViewSpace(const Transform& view) = 0;
|
||||
@@ -75,29 +37,33 @@ public:
|
||||
virtual void setLightingOn(bool on) = 0;
|
||||
virtual void setLightProperties(std::uint32_t index, const LightProperties& light) = 0;
|
||||
virtual void setFogOn(bool on) = 0;
|
||||
virtual void setFogAttributes(float start, float end, std::uint32_t color) = 0;
|
||||
virtual void setFogAttributes(float start, float end, Color color) = 0;
|
||||
virtual void setTextureTransformMatrixIdentity(std::uint32_t stage) = 0; // slot 20
|
||||
virtual void setTextureTransformMatrix(std::uint32_t stage, const TextureTransform& transform) = 0;
|
||||
virtual void setEnvironmentAmbient(std::uint32_t color) = 0;
|
||||
virtual PiRenderTexture* newRenderTexture(Pixmap* pixmap, std::uint32_t flags) = 0;
|
||||
virtual void setEnvironmentAmbient(Color color) = 0; // zero alpha
|
||||
virtual PiRenderTexture* newRenderTexture(const Pixmap& pixmap, std::uint32_t flags) = 0;
|
||||
virtual PiInternalIndexBuffer* newIndexBuffer(std::uint32_t count, const std::uint16_t* indices) = 0;
|
||||
virtual PiInternalVertexBuffer* newStaticVertexBuffer(const void* data, std::uint32_t a, std::uint32_t b,
|
||||
std::uint32_t c) = 0; // slot 25
|
||||
virtual PiInternalVertexBuffer* newDynamicVertexBuffer(std::uint32_t a, std::uint32_t b) = 0;
|
||||
virtual PiInternalVertexBuffer* newStaticVertexBuffer(const void* data, std::uint32_t stride,
|
||||
std::uint32_t elementSize,
|
||||
std::uint32_t elementCount) = 0; // slot 25
|
||||
// Filled later with PiInternalVertexBuffer::update.
|
||||
virtual PiInternalVertexBuffer* newDynamicVertexBuffer(std::uint32_t elementSize, std::uint32_t elementCount) = 0;
|
||||
virtual SharedPtr<PiPixmapRenderTarget> newPixmapRenderTarget(Vec2i size) = 0;
|
||||
virtual SharedPtr<PiTextureRenderTarget> newTextureRenderTarget(Vec2i size) = 0;
|
||||
virtual void beginRendering(PiRenderTarget* target) = 0;
|
||||
virtual void endRendering() = 0; // slot 30
|
||||
// Vertex buffers: positions (Vec3f), normals (Vec3f), texture coordinates (Vec2f), colors (Color); all but
|
||||
// positions may be null.
|
||||
virtual void drawTriangles(PiInternalIndexBuffer* indices, PiInternalVertexBuffer* positions,
|
||||
PiInternalVertexBuffer* normals, PiInternalVertexBuffer* texCoords0,
|
||||
PiInternalVertexBuffer* texCoords1) = 0;
|
||||
PiInternalVertexBuffer* normals, PiInternalVertexBuffer* texCoords,
|
||||
PiInternalVertexBuffer* colors) = 0;
|
||||
virtual void drawLine(Vec3f from, Vec3f to, std::uint32_t color) = 0;
|
||||
virtual void drawPointSprites(const PointSpriteVector& sprites) = 0;
|
||||
virtual void drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount,
|
||||
const std::uint16_t* indices, const VertexAttributeArray<Vec3f>& positions,
|
||||
const VertexAttributeArray<Vec3f>& normals,
|
||||
const VertexAttributeArray<Vec2f>& texCoords,
|
||||
const VertexAttributeArray<std::uint32_t>& colors) = 0; // slot 34
|
||||
const VertexAttributeArray<Color>& colors) = 0; // slot 34
|
||||
|
||||
private:
|
||||
// PiRenderEngine's own fields, after the vtable pointer: a magic number IMVU's destructor checks, and a size
|
||||
|
||||
@@ -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