The adapter records everything the client draws (state, transforms, lights, fog, textures and buffers) as a command stream, sent in chunks through the shared memory channel, while IMVU's Direct3D renderer still draws the whole view. The renderer process draws it with Diligent, with shaders reproducing IMVU's Direct3D 9 fixed-function setup, over the right half of the 3D view to compare. It skips to the newest frame when it falls behind, and logs its errors and frame times. Co-Authored-By: Claude Opus 5.5 <[email protected]>
245 lines
9.5 KiB
C++
245 lines
9.5 KiB
C++
// t5de-renderer.exe: the renderer process. Started by the adapter (t5deRenderEngine.dll) for each of the client's
|
|
// 3D views: receives the client's drawing as commands through a shared memory channel, and draws it with Diligent
|
|
// into a child window it places in the client's window (over the right half, for now, to compare with the client's
|
|
// own renderer on the left).
|
|
|
|
#include <windows.h>
|
|
|
|
#include <share.h>
|
|
#include <shellapi.h>
|
|
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <format>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
#include "EngineFactoryD3D11.h"
|
|
#include "RefCntAutoPtr.hpp"
|
|
#include "scene.hpp"
|
|
#include "t5de/protocol/channel.hpp"
|
|
|
|
namespace {
|
|
|
|
using namespace Diligent;
|
|
using t5de::protocol::Channel;
|
|
using t5de::protocol::View;
|
|
|
|
// The renderer processes' log: %TEMP%\t5de-renderer-process.log, shared by all of them (lines start with the pid).
|
|
void write_log(std::string_view line)
|
|
{
|
|
static std::FILE* file = [] {
|
|
wchar_t temp[MAX_PATH + 1];
|
|
const DWORD length = GetTempPathW(MAX_PATH + 1, temp);
|
|
return length == 0 || length > MAX_PATH
|
|
? nullptr
|
|
: _wfsopen((std::filesystem::path(temp) / L"t5de-renderer-process.log").c_str(), L"a", _SH_DENYNO);
|
|
}();
|
|
if (file != nullptr) {
|
|
std::fprintf(file, "%lu: %.*s\n", GetCurrentProcessId(), static_cast<int>(line.size()), line.data());
|
|
std::fflush(file);
|
|
}
|
|
}
|
|
|
|
template <class... Args>
|
|
void log(std::format_string<Args...> format, Args&&... args)
|
|
{
|
|
write_log(std::format(format, std::forward<Args>(args)...));
|
|
}
|
|
|
|
// Diligent's messages (shader compilation errors, invalid states, ...), into the log.
|
|
void DILIGENT_CALL_TYPE diligent_message(DEBUG_MESSAGE_SEVERITY severity, const Char* message, const Char* function,
|
|
const Char*, int)
|
|
{
|
|
static constexpr const char* severities[] = {"info", "warning", "error", "fatal error"};
|
|
log("Diligent {}: {}{}{}", severities[severity < 4 ? severity : 3], function != nullptr ? function : "",
|
|
function != nullptr ? ": " : "", message != nullptr ? message : "");
|
|
}
|
|
|
|
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,
|
|
0, 0, 1, 1, parent, nullptr, windowClass.hInstance, nullptr);
|
|
}
|
|
|
|
// Where the child window goes in the client's 3D view.
|
|
RECT placement(HWND parent, View view)
|
|
{
|
|
RECT client{};
|
|
GetClientRect(parent, &client);
|
|
if (view == View::Split) {
|
|
client.left = client.right / 2;
|
|
}
|
|
return client;
|
|
}
|
|
|
|
} // 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<Channel*>(MapViewOfFile(mapping, FILE_MAP_ALL_ACCESS, 0, 0, 0));
|
|
HANDLE ready = OpenEventW(SYNCHRONIZE, FALSE, (arguments.channel + t5de::protocol::readyEventSuffix).c_str());
|
|
HANDLE consumed =
|
|
OpenEventW(EVENT_MODIFY_STATE, FALSE, (arguments.channel + t5de::protocol::consumedEventSuffix).c_str());
|
|
HANDLE parent = OpenProcess(SYNCHRONIZE, FALSE, arguments.parent);
|
|
if (channel == nullptr || ready == nullptr || consumed == nullptr || parent == nullptr
|
|
|| channel->version != t5de::protocol::version) {
|
|
return 3;
|
|
}
|
|
const auto* data = reinterpret_cast<const std::byte*>(channel + 1);
|
|
|
|
const HWND clientWindow = reinterpret_cast<HWND>(static_cast<std::uintptr_t>(channel->window));
|
|
const HWND window = create_child_window(clientWindow);
|
|
if (window == nullptr) {
|
|
return 4;
|
|
}
|
|
|
|
log("started for window {:#x}, view {}", channel->window, static_cast<int>(channel->view));
|
|
IEngineFactoryD3D11* factory = GetEngineFactoryD3D11();
|
|
factory->SetMessageCallback(diligent_message);
|
|
RefCntAutoPtr<IRenderDevice> device;
|
|
RefCntAutoPtr<IDeviceContext> context;
|
|
EngineD3D11CreateInfo engineInfo;
|
|
factory->CreateDeviceAndContextsD3D11(engineInfo, &device, &context);
|
|
if (!device) {
|
|
log("can't create a Direct3D 11 device");
|
|
return 5;
|
|
}
|
|
RefCntAutoPtr<ISwapChain> swapChain;
|
|
SwapChainDesc swapChainDesc;
|
|
swapChainDesc.ColorBufferFormat = TEX_FORMAT_RGBA8_UNORM; // no sRGB conversion, like the client's renderer
|
|
swapChainDesc.DepthBufferFormat = TEX_FORMAT_D32_FLOAT;
|
|
factory->CreateSwapChainD3D11(device, context, swapChainDesc, FullScreenModeDesc{}, Win32NativeWindow{window},
|
|
&swapChain);
|
|
if (!swapChain) {
|
|
log("can't create a swap chain");
|
|
return 6;
|
|
}
|
|
t5de::Scene scene(device, context, swapChain);
|
|
|
|
// Takes the chunk waiting in the channel (after "ready"), and lets the adapter send the next: appends it to
|
|
// frame, and returns whether it ends a frame.
|
|
std::vector<std::byte> frame;
|
|
const auto take_chunk = [&] {
|
|
const std::uint32_t size = channel->chunkSize <= channel->capacity ? channel->chunkSize : 0;
|
|
const bool endsFrame = (channel->chunkFlags & t5de::protocol::chunkEndsFrame) != 0;
|
|
frame.insert(frame.end(), data, data + size);
|
|
SetEvent(consumed);
|
|
return endsFrame;
|
|
};
|
|
|
|
LARGE_INTEGER frequency;
|
|
QueryPerformanceFrequency(&frequency);
|
|
double drawTime = 0;
|
|
int drawn = 0;
|
|
int skipped = 0;
|
|
RECT placed{};
|
|
for (;;) {
|
|
const HANDLE handles[] = {ready, 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 || !take_chunk()) {
|
|
continue;
|
|
}
|
|
|
|
// If the client has sent more frames meanwhile (drawing fell behind), catch up: carry out the older ones'
|
|
// changes without drawing them, and draw only the newest. (An incomplete frame stays for the next round.)
|
|
std::size_t complete = frame.size();
|
|
while (WaitForSingleObject(ready, 0) == WAIT_OBJECT_0) {
|
|
bool ended = take_chunk();
|
|
while (!ended && WaitForSingleObject(ready, 1000) == WAIT_OBJECT_0) {
|
|
ended = take_chunk();
|
|
}
|
|
if (!ended) {
|
|
break;
|
|
}
|
|
scene.execute(std::span(frame).first(complete), 0, false);
|
|
frame.erase(frame.begin(), frame.begin() + static_cast<std::ptrdiff_t>(complete));
|
|
complete = frame.size();
|
|
++skipped;
|
|
}
|
|
|
|
// Follow the client's 3D view's size.
|
|
const RECT place = placement(clientWindow, channel->view);
|
|
const int width = place.right - place.left;
|
|
const int height = place.bottom - place.top;
|
|
if (std::memcmp(&place, &placed, sizeof(RECT)) != 0 && width > 0 && height > 0) {
|
|
SetWindowPos(window, nullptr, place.left, place.top, width, height, SWP_NOZORDER | SWP_NOACTIVATE);
|
|
swapChain->Resize(static_cast<Uint32>(width), static_cast<Uint32>(height));
|
|
placed = place;
|
|
log("drawing {}x{} at {} in the client's 3D view", width, height, place.left);
|
|
}
|
|
LARGE_INTEGER start, end;
|
|
QueryPerformanceCounter(&start);
|
|
if (scene.execute(std::span(frame).first(complete), place.left, width > 0 && height > 0)) {
|
|
swapChain->Present(0);
|
|
QueryPerformanceCounter(&end);
|
|
drawTime += static_cast<double>(end.QuadPart - start.QuadPart) / static_cast<double>(frequency.QuadPart);
|
|
if (++drawn == 300) {
|
|
log("last 300 frames: {:.1f} ms each to draw and present, {} skipped to catch up", drawTime * 1000 / drawn,
|
|
skipped);
|
|
drawTime = 0;
|
|
drawn = 0;
|
|
skipped = 0;
|
|
}
|
|
}
|
|
frame.erase(frame.begin(), frame.begin() + static_cast<std::ptrdiff_t>(complete));
|
|
}
|
|
|
|
log("exiting");
|
|
DestroyWindow(window);
|
|
return 0;
|
|
}
|