Draw all of a client's views in one renderer process
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]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-28 19:16:01 -07:00
1 parent 6e4fe589e5
commit 8055099920
5 files changed
+404 -195

No files matched your search

+86 -13
View File
@@ -1,10 +1,10 @@
#include "renderer_process.hpp" #include "renderer_process.hpp"
#include <algorithm> #include <algorithm>
#include <atomic>
#include <cstring> #include <cstring>
#include <filesystem> #include <filesystem>
#include <format> #include <format>
#include <mutex>
#include <string> #include <string>
#include "log.hpp" #include "log.hpp"
@@ -54,12 +54,92 @@ std::filesystem::path running_copy(const std::filesystem::path& exe)
return copy; 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<protocol::Control*>(
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 } // namespace
std::unique_ptr<RendererProcess> RendererProcess::start(HWND window, protocol::View view) std::unique_ptr<RendererProcess> RendererProcess::start(HWND window, protocol::View view)
{ {
static std::atomic<int> engines = 0; Host& host = Host::get();
const std::wstring name = std::format(L"Local\\t5de-renderer-{}-{}", GetCurrentProcessId(), engines++); const std::scoped_lock lock(host.mutex());
const std::wstring name = host.nextChannel();
constexpr DWORD size = sizeof(protocol::Channel) + protocol::channelCapacity; constexpr DWORD size = sizeof(protocol::Channel) + protocol::channelCapacity;
std::unique_ptr<RendererProcess> renderer(new RendererProcess()); std::unique_ptr<RendererProcess> renderer(new RendererProcess());
@@ -95,18 +175,11 @@ std::unique_ptr<RendererProcess> 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 // 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. // 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); SetWindowLongW(window, GWL_STYLE, GetWindowLongW(window, GWL_STYLE) & ~WS_CLIPCHILDREN);
const auto exe = running_copy(module_folder() / L"t5de-renderer.exe"); renderer->process_ = host.attach();
std::wstring command = std::format(L"\"{}\" --channel {} --parent {}", exe.wstring(), name, GetCurrentProcessId()); if (renderer->process_ == nullptr) {
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 nullptr; return nullptr;
} }
CloseHandle(process.hThread); log::info("renderer process: drawing channel {}", narrow(name));
renderer->process_ = process.hProcess;
log::info("renderer process: started {} (pid {}) on channel {}", exe.string(), process.dwProcessId, narrow(name));
return renderer; return renderer;
} }
+5 -3
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
// The renderer process (t5de-renderer.exe, 64-bit) an engine draws through: started with the engine, sent the // The renderer process (t5de-renderer.exe, 64-bit) an engine draws through: one for the whole client, started with
// command stream through a shared memory channel (include/t5de/protocol/channel.hpp), and told to exit with it. // 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 <windows.h> #include <windows.h>
@@ -14,7 +15,8 @@ namespace t5de {
class RendererProcess { class RendererProcess {
public: 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<RendererProcess> start(HWND window, protocol::View view); static std::unique_ptr<RendererProcess> start(HWND window, protocol::View view);
RendererProcess(const RendererProcess&) = delete; RendererProcess(const RendererProcess&) = delete;
+12 -8
View File
@@ -19,13 +19,17 @@ Direct3D 11 backend is built for now.
## How they talk ## How they talk
`include/t5de/protocol/` is shared by both builds, so it uses fixed-size fields only. For each engine the client `include/t5de/protocol/` is shared by both builds, so it uses fixed-size fields only. One renderer process draws all
creates (each 3D view, and again on room changes), the adapter: of a client's 3D views, with one Direct3D device. The adapter starts `t5de-renderer.exe --control <name> --parent
<pid>` 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-<pid>`, `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-<pid>-<n>`) holding IMVU's window handle and a frame 1. creates a shared memory channel (`<control name>-<n>`) holding IMVU's window handle, and its events;
count, and an event set after each frame; 2. counts it in the control section and sets `<control name>-attach`, and the renderer process opens it, with a
2. starts `t5de-renderer.exe --channel <name> --parent <pid>` from its own folder; child window, swap chain and scene of its own;
3. sets the event at each `endRendering`, and sets `closing` when the engine is destroyed. 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 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 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): 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), 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. 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 ## 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 `-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. 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 ## Building
+15 -3
View File
@@ -7,7 +7,19 @@
namespace t5de::protocol { 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 <name>` argument, holds this; each engine's channel is named
// "<name>-<n>", n counting from 0. Having created a channel, the adapter increments channels and sets the event
// "<name>-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 // 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. // 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 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 <name>` // A shared memory section the adapter creates for each engine (named as Control says): this header, then `capacity`
// argument: this header, then `capacity` bytes of data. The adapter sends the command stream (commands.hpp) through // bytes of data. The adapter sends the command stream (commands.hpp) through
// it in chunks: it waits for the event "<name>-consumed", writes a chunk and sets "<name>-ready"; the renderer // it in chunks: it waits for the event "<name>-consumed", writes a chunk and sets "<name>-ready"; the renderer
// process copies the chunk out and sets "<name>-consumed" (both auto-reset; consumed starts set). // process copies the chunk out and sets "<name>-consumed" (both auto-reset; consumed starts set).
// //
+286 -168
View File
@@ -1,7 +1,6 @@
// t5de-renderer.exe: the renderer process. Started by the adapter (t5deRenderEngine.dll) for each of the client's // t5de-renderer.exe: the renderer process. Started by the adapter (t5deRenderEngine.dll), one for the whole client:
// 3D views: receives the client's drawing as commands through a shared memory channel, and draws it with Diligent // 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 in the client's window (over the right half, for now, to compare with the client's // into a child window it places over the view in the client's window.
// own renderer on the left).
#include <windows.h> #include <windows.h>
@@ -9,10 +8,13 @@
#include <dxgi.h> #include <dxgi.h>
#include <shellapi.h> #include <shellapi.h>
#include <algorithm>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <filesystem> #include <filesystem>
#include <format> #include <format>
#include <memory>
#include <optional>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <vector> #include <vector>
@@ -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"}; static constexpr const char* severities[] = {"info", "warning", "error", "fatal error"};
info("Diligent {}: {}{}{}", severities[severity < 4 ? severity : 3], function != nullptr ? function : "", info("Diligent {}: {}{}{}", severities[severity < 4 ? severity : 3], function != nullptr ? function : "",
function != nullptr ? ": " : "", message != nullptr ? message : ""); function != nullptr ? ": " : "", message != nullptr ? message : "");
} }
struct Arguments { struct Arguments {
std::wstring channel; std::wstring control;
DWORD parent = 0; DWORD parent = 0;
}; };
@@ -53,8 +55,8 @@ Arguments parse_arguments()
wchar_t** argv = CommandLineToArgvW(GetCommandLineW(), &count); wchar_t** argv = CommandLineToArgvW(GetCommandLineW(), &count);
for (int i = 1; i + 1 < count; i += 2) { for (int i = 1; i + 1 < count; i += 2) {
const std::wstring_view name = argv[i]; const std::wstring_view name = argv[i];
if (name == L"--channel") { if (name == L"--control") {
arguments.channel = argv[i + 1]; arguments.control = argv[i + 1];
} else if (name == L"--parent") { } else if (name == L"--parent") {
arguments.parent = static_cast<DWORD>(std::wcstoul(argv[i + 1], nullptr, 10)); arguments.parent = static_cast<DWORD>(std::wcstoul(argv[i + 1], nullptr, 10));
} }
@@ -63,6 +65,8 @@ Arguments parse_arguments()
return 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) LRESULT CALLBACK window_procedure(HWND window, UINT message, WPARAM wparam, LPARAM lparam)
{ {
if (message == WM_ERASEBKGND) { 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. // 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) HWND create_child_window(HWND parent)
{ {
WNDCLASSEXW windowClass{.cbSize = sizeof(WNDCLASSEXW), static const ATOM windowClass = [] {
.lpfnWndProc = window_procedure, const WNDCLASSEXW description{.cbSize = sizeof(WNDCLASSEXW),
.hInstance = GetModuleHandleW(nullptr), .lpfnWndProc = window_procedure,
.hCursor = LoadCursorW(nullptr, IDC_ARROW), .hInstance = GetModuleHandleW(nullptr),
.lpszClassName = L"T5DERenderer"}; .hCursor = LoadCursorW(nullptr, IDC_ARROW),
RegisterClassExW(&windowClass); .lpszClassName = L"T5DERenderer"};
return CreateWindowExW(0, windowClass.lpszClassName, L"", WS_CHILD | WS_VISIBLE | WS_DISABLED | WS_CLIPSIBLINGS, return RegisterClassExW(&description);
0, 0, 1, 1, parent, nullptr, windowClass.hInstance, nullptr); }();
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 // 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. // or compare): for comparing the renderers without restarting the client. Read every second or so.
View current_view(View adapter) View current_view(View adapter)
{ {
static View view = adapter; static std::optional<View> view;
static ULONGLONG checked = 0; static ULONGLONG checked = 0;
if (GetTickCount64() - checked < 1000) { if (GetTickCount64() - checked >= 1000) {
return view; 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(); return view.value_or(adapter);
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 // 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). // one, replaces the built-in shaders, and is read again when it changes (checked every second or so). Returns the
void reload_shaders(t5de::Scene& scene) // new source when it has changed.
std::optional<std::string> changed_shaders()
{ {
static ULONGLONG checked = 0; static ULONGLONG checked = 0;
static std::filesystem::file_time_type loaded{}; static std::filesystem::file_time_type loaded{};
if (GetTickCount64() - checked < 1000) { if (GetTickCount64() - checked < 1000) {
return; return std::nullopt;
} }
checked = GetTickCount64(); checked = GetTickCount64();
std::error_code error; std::error_code error;
const auto path = std::filesystem::temp_directory_path(error) / L"t5de-renderer-shaders" / L"fixed_function.hlsl"; 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); const auto modified = std::filesystem::last_write_time(path, error);
if (error || modified == loaded) { if (error || modified == loaded) {
return; return std::nullopt;
} }
const HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, const HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) { if (file == INVALID_HANDLE_VALUE) {
return; return std::nullopt;
} }
std::string source(static_cast<std::size_t>(std::filesystem::file_size(path, error)), ' '); std::string source(static_cast<std::size_t>(std::filesystem::file_size(path, error)), ' ');
DWORD read = 0; DWORD read = 0;
@@ -146,8 +154,8 @@ void reload_shaders(t5de::Scene& scene)
CloseHandle(file); CloseHandle(file);
source.resize(read); source.resize(read);
loaded = modified; loaded = modified;
scene.setShaderSource(std::move(source));
info("shaders reloaded from {}", path.string()); 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). // 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; 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 } // namespace
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{ {
const Arguments arguments = parse_arguments(); const Arguments arguments = parse_arguments();
if (arguments.channel.empty()) { if (arguments.control.empty()) {
return 2; return 2;
} }
HANDLE mapping = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, arguments.channel.c_str()); HANDLE mapping = OpenFileMappingW(FILE_MAP_READ, FALSE, arguments.control.c_str());
auto* channel = mapping == nullptr ? nullptr const auto* control = mapping == nullptr ? nullptr
: static_cast<Channel*>(MapViewOfFile(mapping, FILE_MAP_ALL_ACCESS, 0, 0, 0)); : static_cast<const t5de::protocol::Control*>(
HANDLE ready = OpenEventW(SYNCHRONIZE, FALSE, (arguments.channel + t5de::protocol::readyEventSuffix).c_str()); MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0));
HANDLE consumed = HANDLE attach = OpenEventW(SYNCHRONIZE, FALSE, (arguments.control + t5de::protocol::attachEventSuffix).c_str());
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 parent = OpenProcess(SYNCHRONIZE, FALSE, arguments.parent); HANDLE parent = OpenProcess(SYNCHRONIZE, FALSE, arguments.parent);
if (channel == nullptr || ready == nullptr || consumed == nullptr || read == nullptr || parent == nullptr if (control == nullptr || attach == nullptr || parent == nullptr || control->version != t5de::protocol::version) {
|| channel->version != t5de::protocol::version) {
return 3; return 3;
} }
auto* data = reinterpret_cast<std::byte*>(channel + 1);
const HWND clientWindow = reinterpret_cast<HWND>(static_cast<std::uintptr_t>(channel->window)); info("started for client {}", arguments.parent);
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(); IEngineFactoryD3D11* factory = GetEngineFactoryD3D11();
factory->SetMessageCallback(diligent_message); factory->SetMessageCallback(diligent_message);
RefCntAutoPtr<IRenderDevice> device; RefCntAutoPtr<IRenderDevice> device;
@@ -217,117 +402,50 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
dxgi->Release(); 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);
// Frames read back: the adapter waits for them, sending nothing meanwhile, so the data area is free.
scene.setReadBack([&](std::span<const std::byte> pixels) {
const std::size_t size = pixels.size() <= channel->capacity ? pixels.size() : 0;
std::memcpy(data, pixels.data(), size);
channel->readSize = static_cast<std::uint32_t>(size);
SetEvent(read);
});
// Takes the chunk waiting in the channel (after "ready"), and lets the adapter send the next: appends it to std::vector<std::unique_ptr<ViewWindow>> views;
// frame, and returns whether it ends a frame. std::uint32_t attached = 0;
std::vector<std::byte> frame; std::optional<std::string> shaders; // the override, if any
const auto take_chunk = [&] { std::size_t first = 0; // which view's event is waited on first, taking turns
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 (;;) { for (;;) {
const HANDLE handles[] = {ready, parent}; // New channels: opened in order; those whose engines are gone already are skipped.
const DWORD result = MsgWaitForMultipleObjects(2, handles, FALSE, 1000, QS_ALLINPUT); 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; MSG message;
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) { while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
DispatchMessageW(&message); DispatchMessageW(&message);
} }
if (result == WAIT_OBJECT_0 + 1 || channel->closing != 0) { if (result == WAIT_OBJECT_0) {
break; // the client exited, or the engine is gone break; // the client exited
} }
if (result != WAIT_OBJECT_0 || !take_chunk()) { if (result >= WAIT_OBJECT_0 + 2 && result < WAIT_OBJECT_0 + handles.size()) {
continue; 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<std::ptrdiff_t>(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<Uint32>(width), static_cast<Uint32>(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<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));
} }
channel->covering = 0; views.clear();
info("exiting"); info("exiting");
DestroyWindow(window);
return 0; return 0;
} }