From 80550999206759ed149d009f8f212a2b04bda30f Mon Sep 17 00:00:00 2001 From: dhkatz Date: Wed, 28 Oct 2026 19:16:01 -0700 Subject: [PATCH] Draw all of a client's views in one renderer process 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 --- adapter/renderer_process.cpp | 99 ++++++- adapter/renderer_process.hpp | 8 +- docs/architecture.md | 20 +- include/t5de/protocol/channel.hpp | 18 +- renderer/main.cpp | 454 +++++++++++++++++++----------- 5 files changed, 404 insertions(+), 195 deletions(-) diff --git a/adapter/renderer_process.cpp b/adapter/renderer_process.cpp index 09ebef2..f14f4e1 100644 --- a/adapter/renderer_process.cpp +++ b/adapter/renderer_process.cpp @@ -1,10 +1,10 @@ #include "renderer_process.hpp" #include -#include #include #include #include +#include #include #include "log.hpp" @@ -54,12 +54,92 @@ std::filesystem::path running_copy(const std::filesystem::path& exe) return copy; } +// The client's one renderer process, and the control section (protocol::Control) engines attach their channels +// through. Started with the first engine; started again by the next one if it has exited. +class Host { +public: + static Host& get() + { + static Host host; + return host; + } + + std::mutex& mutex() { return mutex_; } + + // The next channel's name. + std::wstring nextChannel() const { return std::format(L"{}-{}", name_, channels_); } + + // Tells the renderer process about the channel just created (nextChannel()), starting it if it isn't running. + // Returns a handle to the renderer process, the caller's to close; null if it can't. + HANDLE attach() + { + if (control_ == nullptr || (!running() && !startProcess())) { + return nullptr; + } + control_->channels = ++channels_; + SetEvent(attach_); + HANDLE process = nullptr; + DuplicateHandle(GetCurrentProcess(), process_, GetCurrentProcess(), &process, 0, FALSE, DUPLICATE_SAME_ACCESS); + return process; + } + +private: + Host() : name_(std::format(L"Local\\t5de-renderer-{}", GetCurrentProcessId())) + { + mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, sizeof(protocol::Control), + name_.c_str()); + attach_ = CreateEventW(nullptr, FALSE, FALSE, (name_ + protocol::attachEventSuffix).c_str()); + if (mapping_ != nullptr && attach_ != nullptr) { + control_ = static_cast( + MapViewOfFile(mapping_, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(protocol::Control))); + } + if (control_ == nullptr) { + log::info("renderer process: can't create the control section: error {}", GetLastError()); + return; + } + *control_ = {.version = protocol::version, .channels = 0}; + } + + [[nodiscard]] bool running() const { return process_ != nullptr && WaitForSingleObject(process_, 0) == WAIT_TIMEOUT; } + + bool startProcess() + { + if (process_ != nullptr) { + CloseHandle(process_); + process_ = nullptr; + } + const auto exe = running_copy(module_folder() / L"t5de-renderer.exe"); + std::wstring command = + std::format(L"\"{}\" --control {} --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 false; + } + CloseHandle(process.hThread); + process_ = process.hProcess; + log::info("renderer process: started {} (pid {})", exe.string(), process.dwProcessId); + return true; + } + + std::mutex mutex_; + std::wstring name_; + HANDLE mapping_ = nullptr; + HANDLE attach_ = nullptr; + protocol::Control* control_ = nullptr; + HANDLE process_ = nullptr; + std::uint32_t channels_ = 0; +}; + } // namespace std::unique_ptr RendererProcess::start(HWND window, protocol::View view) { - static std::atomic engines = 0; - const std::wstring name = std::format(L"Local\\t5de-renderer-{}-{}", GetCurrentProcessId(), engines++); + Host& host = Host::get(); + const std::scoped_lock lock(host.mutex()); + const std::wstring name = host.nextChannel(); constexpr DWORD size = sizeof(protocol::Channel) + protocol::channelCapacity; std::unique_ptr renderer(new RendererProcess()); @@ -95,18 +175,11 @@ std::unique_ptr RendererProcess::start(HWND window, protocol::V // WM_PAINT, which a window entirely covered by clipped children never gets (it then drew about once a second). The // renderer's swap chain is a flip model one, which the compositor shows above the client's drawing anyway. SetWindowLongW(window, GWL_STYLE, GetWindowLongW(window, GWL_STYLE) & ~WS_CLIPCHILDREN); - const auto exe = running_copy(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()); + renderer->process_ = host.attach(); + if (renderer->process_ == nullptr) { return nullptr; } - CloseHandle(process.hThread); - renderer->process_ = process.hProcess; - log::info("renderer process: started {} (pid {}) on channel {}", exe.string(), process.dwProcessId, narrow(name)); + log::info("renderer process: drawing channel {}", narrow(name)); return renderer; } diff --git a/adapter/renderer_process.hpp b/adapter/renderer_process.hpp index 4ff1a3c..e703330 100644 --- a/adapter/renderer_process.hpp +++ b/adapter/renderer_process.hpp @@ -1,6 +1,7 @@ #pragma once -// The renderer process (t5de-renderer.exe, 64-bit) an engine draws through: started with the engine, sent the -// command stream through a shared memory channel (include/t5de/protocol/channel.hpp), and told to exit with it. +// The renderer process (t5de-renderer.exe, 64-bit) an engine draws through: one for the whole client, started with +// the first engine; each engine sends it the command stream through a shared memory channel of its own +// (include/t5de/protocol/channel.hpp), and closes the channel when it's destroyed. #include @@ -14,7 +15,8 @@ namespace t5de { class RendererProcess { public: - // Starts t5de-renderer.exe from this DLL's folder, drawing into part of window; null if it can't. + // A channel to the renderer process (t5de-renderer.exe from this DLL's folder, started if it isn't running), + // drawing into part of window; null if it can't. static std::unique_ptr start(HWND window, protocol::View view); RendererProcess(const RendererProcess&) = delete; diff --git a/docs/architecture.md b/docs/architecture.md index 9af4d96..60559b0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -19,13 +19,17 @@ 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: +`include/t5de/protocol/` is shared by both builds, so it uses fixed-size fields only. One renderer process draws all +of a client's 3D views, with one Direct3D device. The adapter starts `t5de-renderer.exe --control --parent +` from its own folder with the first engine (again with the next one if it has exited), and creates a control +section (`Local\t5de-renderer-`, `protocol::Control`). For each engine the client creates (each 3D view: Home's +avatar, the room, ...), 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. +1. creates a shared memory channel (`-`) holding IMVU's window handle, and its events; +2. counts it in the control section and sets `-attach`, and the renderer process opens it, with a + child window, swap chain and scene of its own; +3. sends each frame's commands through it, and sets `closing` when the engine is destroyed (the renderer process + then closes the view). 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 @@ -34,7 +38,7 @@ compositor shows above the client's own drawing. The client's window must not cl which the client creates it with, so the adapter clears it): the client draws a frame when its window gets `WM_PAINT` (`SceneWindow.dll` invalidates it, and draws on the paint), and a window entirely covered by clipped children never gets one, so the client fell to about one frame a second. -The process exits when the engine is destroyed or the client exits. +The process exits when the client exits. ## Status @@ -76,7 +80,7 @@ pipelines draw both), and a pixmap target's frame is read back through the chann `-read` event) into its Pixmap, as the client's renderer does, synchronously. A read back must fit in the channel's data area (16 MB: the client's 2048x2048 snapshots just do). Checked with outfit pictures and photos. -Next: one renderer process for all of a client's views. +Next: sharing pipelines between views (each view's scene compiles its own), and higher frame rates. ## Building diff --git a/include/t5de/protocol/channel.hpp b/include/t5de/protocol/channel.hpp index 1f606c1..03c03d7 100644 --- a/include/t5de/protocol/channel.hpp +++ b/include/t5de/protocol/channel.hpp @@ -7,7 +7,19 @@ namespace t5de::protocol { -inline constexpr std::uint32_t version = 4; +inline constexpr std::uint32_t version = 5; + +// One renderer process draws all of a client's 3D views. A shared memory section the adapter creates once per client +// process, named by the renderer process's `--control ` argument, holds this; each engine's channel is named +// "-", n counting from 0. Having created a channel, the adapter increments channels and sets the event +// "-attach" (auto-reset); the renderer process opens the channels it hasn't yet (those already gone are +// skipped), and stops drawing a channel's view when its engine sets closing. It exits with the client. +struct Control { + std::uint32_t version; + std::uint32_t channels; // channels created so far +}; + +inline constexpr wchar_t attachEventSuffix[] = L"-attach"; // Which part of the client's 3D view the renderer process draws. Full is the default; the others are for comparing // with the client's renderer, which then draws the whole view under the renderer process's window. @@ -18,8 +30,8 @@ enum class View : std::uint32_t { Compare, // over the right half, the left half's scene: both renderers side by side, showing the same }; -// A shared memory section the adapter creates for each engine, named by the renderer process's `--channel ` -// argument: this header, then `capacity` bytes of data. The adapter sends the command stream (commands.hpp) through +// A shared memory section the adapter creates for each engine (named as Control says): this header, then `capacity` +// bytes of data. The adapter sends the command stream (commands.hpp) through // it in chunks: it waits for the event "-consumed", writes a chunk and sets "-ready"; the renderer // process copies the chunk out and sets "-consumed" (both auto-reset; consumed starts set). // diff --git a/renderer/main.cpp b/renderer/main.cpp index 71af0a1..80bedc1 100644 --- a/renderer/main.cpp +++ b/renderer/main.cpp @@ -1,7 +1,6 @@ -// 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). +// 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 @@ -9,10 +8,13 @@ #include #include +#include #include #include #include #include +#include +#include #include #include #include @@ -38,11 +40,11 @@ void DILIGENT_CALL_TYPE diligent_message(DEBUG_MESSAGE_SEVERITY severity, const { 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 : ""); + function != nullptr ? ": " : "", message != nullptr ? message : ""); } struct Arguments { - std::wstring channel; + std::wstring control; DWORD parent = 0; }; @@ -53,8 +55,8 @@ Arguments parse_arguments() 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]; + if (name == L"--control") { + arguments.control = argv[i + 1]; } else if (name == L"--parent") { arguments.parent = static_cast(std::wcstoul(argv[i + 1], nullptr, 10)); } @@ -63,6 +65,8 @@ Arguments parse_arguments() 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) { @@ -74,71 +78,75 @@ LRESULT CALLBACK window_procedure(HWND window, UINT message, WPARAM wparam, LPAR // 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); + 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 View view = adapter; + static std::optional view; static ULONGLONG checked = 0; - if (GetTickCount64() - checked < 1000) { - return view; + 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; + } + } } - 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; + 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). -void reload_shaders(t5de::Scene& scene) +// 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 changed_shaders() { static ULONGLONG checked = 0; static std::filesystem::file_time_type loaded{}; if (GetTickCount64() - checked < 1000) { - return; + 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; + 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; + return std::nullopt; } std::string source(static_cast(std::filesystem::file_size(path, error)), ' '); DWORD read = 0; @@ -146,8 +154,8 @@ void reload_shaders(t5de::Scene& scene) CloseHandle(file); source.resize(read); loaded = modified; - scene.setShaderSource(std::move(source)); 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). @@ -170,35 +178,212 @@ Placement placement(HWND parent, View view) 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 open(const std::wstring& name, IEngineFactoryD3D11* factory, + IRenderDevice* device, IDeviceContext* context, + const std::optional& shaders) + { + std::unique_ptr 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(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(view->channel_ + 1); + view->clientWindow_ = reinterpret_cast(static_cast(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(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 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(size); + SetEvent(self->read_); + }); + QueryPerformanceFrequency(&view->frequency_); + info("{}: drawing window {:#x}, view {}", view->name_, view->channel_->window, + static_cast(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(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(width), static_cast(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(ticks) / static_cast(frequency_.QuadPart); + }; + drawTime_ += seconds(end.QuadPart - start.QuadPart); + latency_ += seconds(end.QuadPart - static_cast(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(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 swapChain_; + std::unique_ptr scene_; + std::vector 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.channel.empty()) { + if (arguments.control.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 read = OpenEventW(EVENT_MODIFY_STATE, FALSE, (arguments.channel + t5de::protocol::readEventSuffix).c_str()); + HANDLE mapping = OpenFileMappingW(FILE_MAP_READ, FALSE, arguments.control.c_str()); + const auto* control = mapping == nullptr ? nullptr + : static_cast( + 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 (channel == nullptr || ready == nullptr || consumed == nullptr || read == nullptr || parent == nullptr - || channel->version != t5de::protocol::version) { + if (control == nullptr || attach == nullptr || parent == nullptr || control->version != t5de::protocol::version) { return 3; } - 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)); + info("started for client {}", arguments.parent); IEngineFactoryD3D11* factory = GetEngineFactoryD3D11(); factory->SetMessageCallback(diligent_message); RefCntAutoPtr device; @@ -217,117 +402,50 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) 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); - // Frames read back: the adapter waits for them, sending nothing meanwhile, so the data area is free. - scene.setReadBack([&](std::span pixels) { - const std::size_t size = pixels.size() <= channel->capacity ? pixels.size() : 0; - std::memcpy(data, pixels.data(), size); - channel->readSize = static_cast(size); - SetEvent(read); - }); - // 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{}; + std::vector> views; + std::uint32_t attached = 0; + std::optional shaders; // the override, if any + std::size_t first = 0; // which view's event is waited on first, taking turns for (;;) { - const HANDLE handles[] = {ready, parent}; - const DWORD result = MsgWaitForMultipleObjects(2, handles, FALSE, 1000, QS_ALLINPUT); + // 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 handles{parent, attach}; + const std::size_t count = std::min(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(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 + 1 || channel->closing != 0) { - break; // the client exited, or the engine is gone + if (result == WAIT_OBJECT_0) { + break; // the client exited } - if (result != WAIT_OBJECT_0 || !take_chunk()) { - continue; + 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(); } - - // 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); - channel->covering = 0; - info("hidden"); - } - } - reload_shaders(scene); - LARGE_INTEGER start, end; - QueryPerformanceCounter(&start); - const bool covers = current_view(channel->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); - 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)); } - channel->covering = 0; + views.clear(); info("exiting"); - DestroyWindow(window); return 0; }