CI / build (push) Successful in 40s
The adapter starts one renderer process per client, with the first engine (again if it has exited), and attaches each engine's channel to it through a control section (protocol version 5: Control, the -attach event). The renderer process keeps a window, swap chain and scene per channel on one Direct3D device, takes the views' frames in turns, and closes a view when its engine does. Co-Authored-By: Claude Opus 5.5 <[email protected]>
452 lines
19 KiB
C++
452 lines
19 KiB
C++
// t5de-renderer.exe: the renderer process. Started by the adapter (t5deRenderEngine.dll), one for the whole client:
|
|
// receives each 3D view's drawing as commands through a shared memory channel of its own, and draws it with Diligent
|
|
// into a child window it places over the view in the client's window.
|
|
|
|
#include <windows.h>
|
|
|
|
#include <d3d11.h>
|
|
#include <dxgi.h>
|
|
#include <shellapi.h>
|
|
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <format>
|
|
#include <memory>
|
|
#include <optional>
|
|
#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 control;
|
|
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"--control") {
|
|
arguments.control = argv[i + 1];
|
|
} else if (name == L"--parent") {
|
|
arguments.parent = static_cast<DWORD>(std::wcstoul(argv[i + 1], nullptr, 10));
|
|
}
|
|
}
|
|
LocalFree(argv);
|
|
return arguments;
|
|
}
|
|
|
|
std::string narrow(const std::wstring& text) { return std::filesystem::path(text).string(); }
|
|
|
|
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)
|
|
{
|
|
static const ATOM windowClass = [] {
|
|
const WNDCLASSEXW description{.cbSize = sizeof(WNDCLASSEXW),
|
|
.lpfnWndProc = window_procedure,
|
|
.hInstance = GetModuleHandleW(nullptr),
|
|
.hCursor = LoadCursorW(nullptr, IDC_ARROW),
|
|
.lpszClassName = L"T5DERenderer"};
|
|
return RegisterClassExW(&description);
|
|
}();
|
|
return CreateWindowExW(0, MAKEINTATOM(windowClass), L"", WS_CHILD | WS_VISIBLE | WS_DISABLED | WS_CLIPSIBLINGS, 0,
|
|
0, 1, 1, parent, nullptr, GetModuleHandleW(nullptr), 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 std::optional<View> view;
|
|
static ULONGLONG checked = 0;
|
|
if (GetTickCount64() - checked >= 1000) {
|
|
checked = GetTickCount64();
|
|
view.reset();
|
|
wchar_t temp[MAX_PATH + 1];
|
|
const DWORD length = GetTempPathW(MAX_PATH + 1, temp);
|
|
const HANDLE file = length == 0 || length > MAX_PATH
|
|
? INVALID_HANDLE_VALUE
|
|
: 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) {
|
|
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 "));
|
|
if (setting == "off") {
|
|
view = View::Off;
|
|
} else if (setting == "split") {
|
|
view = View::Split;
|
|
} else if (setting == "full") {
|
|
view = View::Full;
|
|
} else if (setting == "compare") {
|
|
view = View::Compare;
|
|
}
|
|
}
|
|
}
|
|
return view.value_or(adapter);
|
|
}
|
|
|
|
// 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). Returns the
|
|
// new source when it has changed.
|
|
std::optional<std::string> changed_shaders()
|
|
{
|
|
static ULONGLONG checked = 0;
|
|
static std::filesystem::file_time_type loaded{};
|
|
if (GetTickCount64() - checked < 1000) {
|
|
return std::nullopt;
|
|
}
|
|
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 std::nullopt;
|
|
}
|
|
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::nullopt;
|
|
}
|
|
std::string source(static_cast<std::size_t>(std::filesystem::file_size(path, error)), ' ');
|
|
DWORD read = 0;
|
|
ReadFile(file, source.data(), static_cast<DWORD>(source.size()), &read, nullptr);
|
|
CloseHandle(file);
|
|
source.resize(read);
|
|
loaded = modified;
|
|
info("shaders reloaded from {}", path.string());
|
|
return source;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// One of the client's 3D views: an engine's channel, and the child window its frames are drawn into.
|
|
class ViewWindow {
|
|
public:
|
|
// Opens the channel named name and sets up its window; null if the channel is gone or it can't.
|
|
static std::unique_ptr<ViewWindow> open(const std::wstring& name, IEngineFactoryD3D11* factory,
|
|
IRenderDevice* device, IDeviceContext* context,
|
|
const std::optional<std::string>& shaders)
|
|
{
|
|
std::unique_ptr<ViewWindow> view(new ViewWindow());
|
|
view->name_ = narrow(name.substr(name.rfind(L'-') + 1));
|
|
view->mapping_ = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, name.c_str());
|
|
if (view->mapping_ == nullptr) {
|
|
return nullptr; // its engine is gone already
|
|
}
|
|
view->channel_ = static_cast<Channel*>(MapViewOfFile(view->mapping_, FILE_MAP_ALL_ACCESS, 0, 0, 0));
|
|
view->ready_ = OpenEventW(SYNCHRONIZE, FALSE, (name + t5de::protocol::readyEventSuffix).c_str());
|
|
view->consumed_ = OpenEventW(EVENT_MODIFY_STATE, FALSE, (name + t5de::protocol::consumedEventSuffix).c_str());
|
|
view->read_ = OpenEventW(EVENT_MODIFY_STATE, FALSE, (name + t5de::protocol::readEventSuffix).c_str());
|
|
if (view->channel_ == nullptr || view->ready_ == nullptr || view->consumed_ == nullptr
|
|
|| view->read_ == nullptr || view->channel_->version != t5de::protocol::version) {
|
|
info("{}: can't open the channel", view->name_);
|
|
return nullptr;
|
|
}
|
|
view->data_ = reinterpret_cast<std::byte*>(view->channel_ + 1);
|
|
view->clientWindow_ = reinterpret_cast<HWND>(static_cast<std::uintptr_t>(view->channel_->window));
|
|
view->window_ = create_child_window(view->clientWindow_);
|
|
if (view->window_ == nullptr) {
|
|
info("{}: can't create a window in {:#x}", view->name_, view->channel_->window);
|
|
return nullptr;
|
|
}
|
|
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{view->window_}, &view->swapChain_);
|
|
if (!view->swapChain_) {
|
|
info("{}: can't create a swap chain", view->name_);
|
|
return nullptr;
|
|
}
|
|
view->scene_ = std::make_unique<t5de::Scene>(device, context, view->swapChain_);
|
|
if (shaders) {
|
|
view->scene_->setShaderSource(*shaders);
|
|
}
|
|
// Frames read back: the adapter waits for them, sending nothing meanwhile, so the data area is free.
|
|
ViewWindow* self = view.get();
|
|
view->scene_->setReadBack([self](std::span<const std::byte> pixels) {
|
|
const std::size_t size = pixels.size() <= self->channel_->capacity ? pixels.size() : 0;
|
|
std::memcpy(self->data_, pixels.data(), size);
|
|
self->channel_->readSize = static_cast<std::uint32_t>(size);
|
|
SetEvent(self->read_);
|
|
});
|
|
QueryPerformanceFrequency(&view->frequency_);
|
|
info("{}: drawing window {:#x}, view {}", view->name_, view->channel_->window,
|
|
static_cast<int>(view->channel_->view));
|
|
return view;
|
|
}
|
|
|
|
ViewWindow(const ViewWindow&) = delete;
|
|
ViewWindow& operator=(const ViewWindow&) = delete;
|
|
|
|
~ViewWindow()
|
|
{
|
|
if (channel_ != nullptr) {
|
|
channel_->covering = 0;
|
|
UnmapViewOfFile(channel_);
|
|
}
|
|
scene_.reset();
|
|
swapChain_.Release();
|
|
if (window_ != nullptr) {
|
|
DestroyWindow(window_);
|
|
info("{}: closed", name_);
|
|
}
|
|
for (HANDLE handle : {ready_, consumed_, read_, mapping_}) {
|
|
if (handle != nullptr) {
|
|
CloseHandle(handle);
|
|
}
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] HANDLE ready() const { return ready_; }
|
|
// Its engine is gone.
|
|
[[nodiscard]] bool closing() const { return channel_->closing != 0; }
|
|
|
|
void setShaderSource(const std::string& source) { scene_->setShaderSource(source); }
|
|
|
|
// After "ready": takes the chunk waiting, and draws the newest frame once one is complete.
|
|
void onReady()
|
|
{
|
|
if (closing() || !takeChunk()) {
|
|
return;
|
|
}
|
|
// 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 (!closing() && WaitForSingleObject(ready_, 0) == WAIT_OBJECT_0) {
|
|
bool ended = takeChunk();
|
|
while (!ended && WaitForSingleObject(ready_, 1000) == WAIT_OBJECT_0) {
|
|
ended = takeChunk();
|
|
}
|
|
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 View view = current_view(channel_->view);
|
|
const auto [place, offset] = placement(clientWindow_, 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<Uint32>(width), static_cast<Uint32>(height));
|
|
info("{}: drawing {}x{} at {} in the client's 3D view, its scene from {}", name_, width, height,
|
|
place.left, offset);
|
|
} else {
|
|
ShowWindow(window_, SW_HIDE);
|
|
channel_->covering = 0;
|
|
info("{}: hidden", name_);
|
|
}
|
|
}
|
|
LARGE_INTEGER start, end;
|
|
QueryPerformanceCounter(&start);
|
|
const bool covers = view == View::Full && width > 0 && height > 0;
|
|
if (scene_->execute(std::span(frame_).first(complete), offset, width > 0 && height > 0)) {
|
|
swapChain_->Present(0);
|
|
channel_->covering = covers ? 1 : 0; // the client's renderer can stop drawing (or must start again)
|
|
QueryPerformanceCounter(&end);
|
|
const auto seconds = [this](LONGLONG ticks) {
|
|
return static_cast<double>(ticks) / static_cast<double>(frequency_.QuadPart);
|
|
};
|
|
drawTime_ += seconds(end.QuadPart - start.QuadPart);
|
|
latency_ += seconds(end.QuadPart - static_cast<LONGLONG>(scene_->lastSentAt()));
|
|
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",
|
|
name_, 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));
|
|
}
|
|
|
|
private:
|
|
ViewWindow() = default;
|
|
|
|
// Takes the chunk waiting in the channel (after "ready"), and lets the adapter send the next: appends it to the
|
|
// frame, and returns whether it ends a frame.
|
|
bool takeChunk()
|
|
{
|
|
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;
|
|
}
|
|
|
|
std::string name_;
|
|
HANDLE mapping_ = nullptr;
|
|
HANDLE ready_ = nullptr;
|
|
HANDLE consumed_ = nullptr;
|
|
HANDLE read_ = nullptr;
|
|
Channel* channel_ = nullptr;
|
|
std::byte* data_ = nullptr;
|
|
HWND clientWindow_ = nullptr;
|
|
HWND window_ = nullptr;
|
|
RefCntAutoPtr<ISwapChain> swapChain_;
|
|
std::unique_ptr<t5de::Scene> scene_;
|
|
std::vector<std::byte> frame_;
|
|
RECT placed_{};
|
|
|
|
LARGE_INTEGER frequency_{};
|
|
double drawTime_ = 0;
|
|
double latency_ = 0;
|
|
int drawn_ = 0;
|
|
int skipped_ = 0;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
|
|
{
|
|
const Arguments arguments = parse_arguments();
|
|
if (arguments.control.empty()) {
|
|
return 2;
|
|
}
|
|
HANDLE mapping = OpenFileMappingW(FILE_MAP_READ, FALSE, arguments.control.c_str());
|
|
const auto* control = mapping == nullptr ? nullptr
|
|
: static_cast<const t5de::protocol::Control*>(
|
|
MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0));
|
|
HANDLE attach = OpenEventW(SYNCHRONIZE, FALSE, (arguments.control + t5de::protocol::attachEventSuffix).c_str());
|
|
HANDLE parent = OpenProcess(SYNCHRONIZE, FALSE, arguments.parent);
|
|
if (control == nullptr || attach == nullptr || parent == nullptr || control->version != t5de::protocol::version) {
|
|
return 3;
|
|
}
|
|
|
|
info("started for client {}", arguments.parent);
|
|
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();
|
|
}
|
|
}
|
|
|
|
std::vector<std::unique_ptr<ViewWindow>> views;
|
|
std::uint32_t attached = 0;
|
|
std::optional<std::string> shaders; // the override, if any
|
|
std::size_t first = 0; // which view's event is waited on first, taking turns
|
|
for (;;) {
|
|
// New channels: opened in order; those whose engines are gone already are skipped.
|
|
while (attached < control->channels) {
|
|
if (auto view = ViewWindow::open(std::format(L"{}-{}", arguments.control, attached), factory, device,
|
|
context, shaders)) {
|
|
views.push_back(std::move(view));
|
|
}
|
|
++attached;
|
|
}
|
|
std::erase_if(views, [](const auto& view) { return view->closing(); });
|
|
if (auto source = changed_shaders()) {
|
|
shaders = std::move(source);
|
|
for (const auto& view : views) {
|
|
view->setShaderSource(*shaders);
|
|
}
|
|
}
|
|
|
|
std::vector<HANDLE> handles{parent, attach};
|
|
const std::size_t count = std::min<std::size_t>(views.size(), MAXIMUM_WAIT_OBJECTS - 3);
|
|
for (std::size_t i = 0; i < count; ++i) {
|
|
handles.push_back(views[(first + i) % views.size()]->ready());
|
|
}
|
|
const DWORD result = MsgWaitForMultipleObjects(static_cast<DWORD>(handles.size()), handles.data(), FALSE, 1000,
|
|
QS_ALLINPUT);
|
|
MSG message;
|
|
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
|
DispatchMessageW(&message);
|
|
}
|
|
if (result == WAIT_OBJECT_0) {
|
|
break; // the client exited
|
|
}
|
|
if (result >= WAIT_OBJECT_0 + 2 && result < WAIT_OBJECT_0 + handles.size()) {
|
|
const std::size_t index = (first + (result - WAIT_OBJECT_0 - 2)) % views.size();
|
|
views[index]->onReady();
|
|
first = (index + 1) % views.size();
|
|
}
|
|
}
|
|
|
|
views.clear();
|
|
info("exiting");
|
|
return 0;
|
|
}
|