// 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 #include #include #include #include #include #include #include #include #include #include #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(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); } // The view, as the adapter set it, or as %TEMP%\t5de-renderer-view.txt says while the client runs (off, split, full // or compare): for comparing the renderers without restarting the client. Read every second or so. View current_view(View adapter) { static View view = adapter; static ULONGLONG checked = 0; if (GetTickCount64() - checked < 1000) { return view; } checked = GetTickCount64(); view = adapter; wchar_t temp[MAX_PATH + 1]; const DWORD length = GetTempPathW(MAX_PATH + 1, temp); if (length == 0 || length > MAX_PATH) { return view; } const HANDLE file = CreateFileW((std::filesystem::path(temp) / L"t5de-renderer-view.txt").c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) { return view; } char text[16] = {}; DWORD read = 0; ReadFile(file, text, sizeof(text) - 1, &read, nullptr); CloseHandle(file); const std::string_view setting(text, std::strcspn(text, "\r\n ")); view = setting == "off" ? View::Off : setting == "split" ? View::Split : setting == "full" ? View::Full : setting == "compare" ? View::Compare : adapter; return view; } // Shader changes to try while the client runs: %TEMP%\t5de-renderer-shaders\fixed_function.hlsl, if there is // one, replaces the built-in shaders, and is read again when it changes (checked every second or so). void reload_shaders(t5de::Scene& scene) { static ULONGLONG checked = 0; static std::filesystem::file_time_type loaded{}; if (GetTickCount64() - checked < 1000) { return; } checked = GetTickCount64(); std::error_code error; const auto path = std::filesystem::temp_directory_path(error) / L"t5de-renderer-shaders" / L"fixed_function.hlsl"; const auto modified = std::filesystem::last_write_time(path, error); if (error || modified == loaded) { return; } const HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) { return; } std::string source(static_cast(std::filesystem::file_size(path, error)), ' '); DWORD read = 0; ReadFile(file, source.data(), static_cast(source.size()), &read, nullptr); CloseHandle(file); source.resize(read); loaded = modified; scene.setShaderSource(std::move(source)); info("shaders reloaded from {}", path.string()); } // Where the child window goes in the client's 3D view, and where in the 3D view its scene starts (offset). struct Placement { RECT window{}; int offset = 0; }; Placement placement(HWND parent, View view) { Placement place; GetClientRect(parent, &place.window); if (view == View::Split || view == View::Compare) { place.window.left = place.window.right / 2; } place.offset = view == View::Compare ? 0 : place.window.left; if (view == View::Off) { place.window = {}; } return place; } } // 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, 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(channel + 1); const HWND clientWindow = reinterpret_cast(static_cast(channel->window)); const HWND window = create_child_window(clientWindow); if (window == nullptr) { return 4; } info("started for window {:#x}, view {}", channel->window, static_cast(channel->view)); IEngineFactoryD3D11* factory = GetEngineFactoryD3D11(); factory->SetMessageCallback(diligent_message); RefCntAutoPtr device; RefCntAutoPtr 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 d3d11{device, IID_RenderDeviceD3D11}) { IDXGIDevice1* dxgi = nullptr; if (SUCCEEDED(d3d11->GetD3D11Device()->QueryInterface(__uuidof(IDXGIDevice1), reinterpret_cast(&dxgi)))) { dxgi->SetMaximumFrameLatency(1); dxgi->Release(); } } RefCntAutoPtr 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 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(complete)); complete = frame.size(); ++skipped; } // Follow the client's 3D view's size. const auto [place, offset] = placement(clientWindow, current_view(channel->view)); const int width = place.right - place.left; const int height = place.bottom - place.top; if (std::memcmp(&place, &placed, sizeof(RECT)) != 0) { placed = place; if (width > 0 && height > 0) { SetWindowPos(window, nullptr, place.left, place.top, width, height, SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW); swapChain->Resize(static_cast(width), static_cast(height)); info("drawing {}x{} at {} in the client's 3D view, its scene from {}", width, height, place.left, offset); } else { ShowWindow(window, SW_HIDE); info("hidden"); } } reload_shaders(scene); LARGE_INTEGER start, end; QueryPerformanceCounter(&start); if (scene.execute(std::span(frame).first(complete), offset, width > 0 && height > 0)) { swapChain->Present(0); QueryPerformanceCounter(&end); drawTime += static_cast(end.QuadPart - start.QuadPart) / static_cast(frequency.QuadPart); latency += static_cast(end.QuadPart - static_cast(scene.lastSentAt())) / static_cast(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(complete)); } info("exiting"); DestroyWindow(window); return 0; }