Particles (point sprites) are drawn as instanced camera-facing squares sized like Direct3D 9's. Fog is by depth buffer value, as Direct3D 9 does with IMVU's projections. At most one frame is queued for display, frames carry the adapter's timestamp so the renderer process logs the delay to presenting, and the log's lines from several processes no longer mix. Co-Authored-By: Claude Opus 5.5 <[email protected]>
240 lines
9.5 KiB
C++
240 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 <d3d11.h>
|
|
#include <dxgi.h>
|
|
#include <shellapi.h>
|
|
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <format>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
#include "EngineFactoryD3D11.h"
|
|
#include "RefCntAutoPtr.hpp"
|
|
#include "RenderDeviceD3D11.h"
|
|
#include "log.hpp"
|
|
#include "scene.hpp"
|
|
#include "t5de/protocol/channel.hpp"
|
|
|
|
namespace {
|
|
|
|
using namespace Diligent;
|
|
using t5de::protocol::Channel;
|
|
using t5de::protocol::View;
|
|
|
|
using t5de::log::info;
|
|
|
|
// 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"};
|
|
info("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;
|
|
}
|
|
|
|
info("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) {
|
|
info("can't create a Direct3D 11 device");
|
|
return 5;
|
|
}
|
|
// Show each frame as soon as it's drawn: Direct3D 11 otherwise lets 3 frames queue, which the view would trail by.
|
|
if (RefCntAutoPtr<IRenderDeviceD3D11> d3d11{device, IID_RenderDeviceD3D11}) {
|
|
IDXGIDevice1* dxgi = nullptr;
|
|
if (SUCCEEDED(d3d11->GetD3D11Device()->QueryInterface(__uuidof(IDXGIDevice1), reinterpret_cast<void**>(&dxgi)))) {
|
|
dxgi->SetMaximumFrameLatency(1);
|
|
dxgi->Release();
|
|
}
|
|
}
|
|
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) {
|
|
info("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;
|
|
double latency = 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;
|
|
info("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);
|
|
latency += static_cast<double>(end.QuadPart - static_cast<LONGLONG>(scene.lastSentAt())) /
|
|
static_cast<double>(frequency.QuadPart);
|
|
if (++drawn == 300) {
|
|
info("last 300 frames: {:.1f} ms each to draw and present, {:.1f} ms from the client's end of frame to "
|
|
"presented, {} skipped to catch up",
|
|
drawTime * 1000 / drawn, latency * 1000 / drawn, skipped);
|
|
drawTime = 0;
|
|
latency = 0;
|
|
drawn = 0;
|
|
skipped = 0;
|
|
}
|
|
}
|
|
frame.erase(frame.begin(), frame.begin() + static_cast<std::ptrdiff_t>(complete));
|
|
}
|
|
|
|
info("exiting");
|
|
DestroyWindow(window);
|
|
return 0;
|
|
}
|