diff --git a/CLAUDE.md b/CLAUDE.md index f1e0e4d..d274992 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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//install`. diff --git a/CMakeLists.txt b/CMakeLists.txt index 92fd6da..423b614 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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$<$: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 - $) -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 $) +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 $) + target_compile_features(t5de_imvu INTERFACE cxx_std_20) + + add_subdirectory(adapter) + if(BUILD_TESTING) + add_subdirectory(tests) + endif() +else() + add_subdirectory(renderer) endif() diff --git a/CMakePresets.json b/CMakePresets.json index 013c2b2..dfdc1d0 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -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" } ] } ] diff --git a/README.md b/README.md index 8a4be95..0ba7cd9 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/adapter/CMakeLists.txt b/adapter/CMakeLists.txt new file mode 100644 index 0000000..5a48310 --- /dev/null +++ b/adapter/CMakeLists.txt @@ -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 .) diff --git a/src/log.cpp b/adapter/log.cpp similarity index 100% rename from src/log.cpp rename to adapter/log.cpp diff --git a/src/log.hpp b/adapter/log.hpp similarity index 100% rename from src/log.hpp rename to adapter/log.hpp diff --git a/src/passthrough_engine.cpp b/adapter/passthrough_engine.cpp similarity index 98% rename from src/passthrough_engine.cpp rename to adapter/passthrough_engine.cpp index 72fd516..14474a8 100644 --- a/src/passthrough_engine.cpp +++ b/adapter/passthrough_engine.cpp @@ -93,7 +93,9 @@ imvu::Vec2i pixmap_size(const imvu::Pixmap& pixmap) } // namespace -PassthroughEngine::PassthroughEngine(imvu::SharedPtr inner) : inner_(std::move(inner)) +PassthroughEngine::PassthroughEngine(imvu::SharedPtr inner, + std::unique_ptr renderer) + : inner_(std::move(inner)), renderer_(std::move(renderer)) { log::info("passthrough: wrapping the engine at {}", static_cast(inner_.get())); } @@ -335,6 +337,9 @@ void PassthroughEngine::endRendering() { count(30); inner_->endRendering(); + if (renderer_) { + renderer_->frameDrawn(); + } if (logged(frames_)) { std::string line = std::format("frame {}:", frames_); for (std::size_t slot = 1; slot < slots; ++slot) { diff --git a/src/passthrough_engine.hpp b/adapter/passthrough_engine.hpp similarity index 94% rename from src/passthrough_engine.hpp rename to adapter/passthrough_engine.hpp index dd65270..e570832 100644 --- a/src/passthrough_engine.hpp +++ b/adapter/passthrough_engine.hpp @@ -4,14 +4,17 @@ #include #include +#include +#include "renderer_process.hpp" #include "t5de/imvu/render_engine.hpp" namespace t5de { class PassthroughEngine final : public imvu::PiRenderEngine { public: - explicit PassthroughEngine(imvu::SharedPtr inner); + // renderer may be null. + PassthroughEngine(imvu::SharedPtr inner, std::unique_ptr renderer); ~PassthroughEngine() override; void resizeFrameBuffer(imvu::Vec2i size) override; @@ -65,6 +68,7 @@ private: void count(std::size_t slot); imvu::SharedPtr inner_; + std::unique_ptr renderer_; std::array calls_{}; std::uint64_t frames_ = 0; }; diff --git a/src/plugin.cpp b/adapter/plugin.cpp similarity index 86% rename from src/plugin.cpp rename to adapter/plugin.cpp index 6b6b4b3..96dd0cc 100644 --- a/src/plugin.cpp +++ b/adapter/plugin.cpp @@ -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 __cdecl createRenderEngine(imvu { try { log::info("createRenderEngine: window {}", static_cast(window.get())); + HWND hwnd = window ? window->getPlatformWindow() : nullptr; auto inner = create_imvu_engine(std::move(window)); if (!inner) { return {}; } - return imvu::SharedPtr(std::make_unique(std::move(inner))); + auto renderer = hwnd != nullptr ? RendererProcess::start(hwnd) : nullptr; + return imvu::SharedPtr( + std::make_unique(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()); diff --git a/src/plugin.def b/adapter/plugin.def similarity index 100% rename from src/plugin.def rename to adapter/plugin.def diff --git a/adapter/renderer_process.cpp b/adapter/renderer_process.cpp new file mode 100644 index 0000000..64cee38 --- /dev/null +++ b/adapter/renderer_process.cpp @@ -0,0 +1,92 @@ +#include "renderer_process.hpp" + +#include +#include +#include +#include + +#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(&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::start(HWND window) +{ + static std::atomic engines = 0; + const std::wstring name = std::format(L"Local\\t5de-renderer-{}-{}", GetCurrentProcessId(), engines++); + + std::unique_ptr 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( + 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(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 diff --git a/adapter/renderer_process.hpp b/adapter/renderer_process.hpp new file mode 100644 index 0000000..470cd0d --- /dev/null +++ b/adapter/renderer_process.hpp @@ -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 + +#include +#include + +#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 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 diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..067ed10 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,47 @@ +# Architecture + +The renderer is two programs: + +- **The adapter**, `t5deRenderEngine.dll` (32-bit, `adapter/`): a renderer plugin IMVU Classic loads in place of its + OpenGL renderer (selected by the `t5de-renderer` patch in the `t5de` repository). It implements IMVU's + `PiRenderEngine` interface (`include/t5de/imvu/`, `docs/interface.md`) and passes what the client draws to the + renderer process. It stays thin: IMVU's process is 32-bit (4 GB of address space shared with Python, Gecko and + IMVU's own code) and tied to Visual C++ 2010's ABI. +- **The renderer process**, `t5de-renderer.exe` (64-bit, `renderer/`): draws, with + [Diligent Engine](https://github.com/DiligentGraphics/DiligentCore) (Apache-2.0). Being 64-bit and separate, it can + use any modern API and library, isn't limited by the client's address space, and can crash without taking the + client down. + +Why Diligent: one API over Direct3D 11 (every Windows PC, and software rendering on machines without a graphics +driver), Direct3D 12 and Vulkan (ray tracing, mesh shaders), and WebGPU (browsers, for a possible future client). +It's built with CMake and fetched by `FetchContent`, pinned to a commit (`renderer/CMakeLists.txt`); only the +Direct3D 11 backend is built for now. + +## How they talk + +`include/t5de/protocol/` is shared by both builds, so it uses fixed-size fields only. For each engine the client +creates (each 3D view, and again on room changes), the adapter: + +1. creates a shared memory channel (`Local\t5de-renderer--`) holding IMVU's window handle and a frame + count, and an event set after each frame; +2. starts `t5de-renderer.exe --channel --parent ` from its own folder; +3. sets the event at each `endRendering`, and sets `closing` when the engine is destroyed. + +The renderer process draws into a child window it creates inside the client's window: window handles are valid in +every process, and Windows allows child windows across processes (Chrome's GPU process works this way). The child +is disabled so mouse input goes to the client's window under it, and the adapter gives the client's window +`WS_CLIPCHILDREN` so the client's own drawing stays out of it. The process exits when the engine is destroyed or the +client exits. + +## Status + +A spike: the adapter still has IMVU's Direct3D 9 engine draw the scene (the pass-through), and the renderer process +only draws a box in the 3D view's corner, pulsing with the client's frames. It proves the risky parts: drawing into +the client's window from another process, keeping in step with the client's frames, and mouse input still reaching +the client. Next: the protocol carrying the frame's calls and resources, and the renderer process drawing the scene. + +## Building + +Two presets, one per architecture: `cmake --workflow --preset adapter` (32-bit, with the tests) and +`cmake --workflow --preset renderer` (64-bit; the first configure fetches and builds Diligent). Both programs go in +the client's folder. diff --git a/include/t5de/imvu/types.hpp b/include/t5de/imvu/types.hpp index c065096..2ae9687 100644 --- a/include/t5de/imvu/types.hpp +++ b/include/t5de/imvu/types.hpp @@ -5,6 +5,8 @@ #include #include +struct HWND__; // windows.h's window handle type (HWND is HWND__*) + namespace t5de::imvu { template @@ -94,6 +96,18 @@ struct PointSpriteVector; // Region lists IMVU passes to texture updates; unknown so far. class CompoundRegion; -class INativeWindow; +struct UiEvent; +using NativeWindowHandle = ::HWND__*; // an HWND + +// IMVU's window (SceneWindow.dll's NativeWindow), as far as a renderer needs it: the first methods of its vtable. +// Its later methods include overloads, which MSVC lays out unlike their declaration: declare them from the vtable +// if they're ever needed. +class INativeWindow { +public: + virtual ~INativeWindow() = default; + virtual void handleMessage(UiEvent* event) = 0; + virtual int onSetCursor(Vec2i position) = 0; + virtual NativeWindowHandle getPlatformWindow() = 0; +}; } // namespace t5de::imvu diff --git a/include/t5de/protocol/channel.hpp b/include/t5de/protocol/channel.hpp new file mode 100644 index 0000000..20155cb --- /dev/null +++ b/include/t5de/protocol/channel.hpp @@ -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 + +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 `; the event "-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 diff --git a/renderer/CMakeLists.txt b/renderer/CMakeLists.txt new file mode 100644 index 0000000..dd08ff5 --- /dev/null +++ b/renderer/CMakeLists.txt @@ -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 .) diff --git a/renderer/main.cpp b/renderer/main.cpp new file mode 100644 index 0000000..5faec89 --- /dev/null +++ b/renderer/main.cpp @@ -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 + +#include + +#include +#include +#include +#include + +#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(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(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(static_cast(channel->window))); + if (window == nullptr) { + return 4; + } + + IEngineFactoryD3D11* factory = GetEngineFactoryD3D11(); + RefCntAutoPtr device; + RefCntAutoPtr context; + EngineD3D11CreateInfo engineInfo; + factory->CreateDeviceAndContextsD3D11(engineInfo, &device, &context); + if (!device) { + return 5; + } + RefCntAutoPtr 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(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; +}