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
+393 -184

No files matched your search

+86 -13
View File
@@ -1,10 +1,10 @@
#include "renderer_process.hpp"
#include <algorithm>
#include <atomic>
#include <cstring>
#include <filesystem>
#include <format>
#include <mutex>
#include <string>
#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<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
std::unique_ptr<RendererProcess> RendererProcess::start(HWND window, protocol::View view)
{
static std::atomic<int> 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<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
// 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;
}
+5 -3
View File
@@ -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 <windows.h>
@@ -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<RendererProcess> start(HWND window, protocol::View view);
RendererProcess(const RendererProcess&) = delete;
+12 -8
View File
@@ -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 <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
count, and an event set after each frame;
2. starts `t5de-renderer.exe --channel <name> --parent <pid>` 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 (`<control name>-<n>`) holding IMVU's window handle, and its events;
2. counts it in the control section and sets `<control name>-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
+15 -3
View File
@@ -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 <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
// 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 <name>`
// 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 "<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).
//
+275 -157
View File
@@ -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 <windows.h>
@@ -9,10 +8,13 @@
#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>
@@ -42,7 +44,7 @@ void DILIGENT_CALL_TYPE diligent_message(DEBUG_MESSAGE_SEVERITY severity, const
}
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<DWORD>(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),
static const ATOM windowClass = [] {
const WNDCLASSEXW description{.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);
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> view;
static ULONGLONG checked = 0;
if (GetTickCount64() - checked < 1000) {
return view;
}
if (GetTickCount64() - checked >= 1000) {
checked = GetTickCount64();
view = adapter;
view.reset();
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;
}
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 "));
view = setting == "off" ? View::Off
: setting == "split" ? View::Split
: setting == "full" ? View::Full
: setting == "compare" ? View::Compare
: adapter;
return view;
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).
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<std::string> 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::size_t>(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<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.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<Channel*>(MapViewOfFile(mapping, FILE_MAP_ALL_ACCESS, 0, 0, 0));
HANDLE ready = OpenEventW(SYNCHRONIZE, FALSE, (arguments.channel + t5de::protocol::readyEventSuffix).c_str());
HANDLE consumed =
OpenEventW(EVENT_MODIFY_STATE, FALSE, (arguments.channel + t5de::protocol::consumedEventSuffix).c_str());
HANDLE 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<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 (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<std::byte*>(channel + 1);
const HWND clientWindow = reinterpret_cast<HWND>(static_cast<std::uintptr_t>(channel->window));
const HWND window = create_child_window(clientWindow);
if (window == nullptr) {
return 4;
}
info("started for window {:#x}, view {}", channel->window, static_cast<int>(channel->view));
info("started for client {}", arguments.parent);
IEngineFactoryD3D11* factory = GetEngineFactoryD3D11();
factory->SetMessageCallback(diligent_message);
RefCntAutoPtr<IRenderDevice> device;
@@ -217,117 +402,50 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
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
// frame, and returns whether it ends a frame.
std::vector<std::byte> frame;
const auto take_chunk = [&] {
const std::uint32_t size = channel->chunkSize <= channel->capacity ? channel->chunkSize : 0;
const bool endsFrame = (channel->chunkFlags & t5de::protocol::chunkEndsFrame) != 0;
frame.insert(frame.end(), data, data + size);
SetEvent(consumed);
return endsFrame;
};
LARGE_INTEGER frequency;
QueryPerformanceFrequency(&frequency);
double drawTime = 0;
double latency = 0;
int drawn = 0;
int skipped = 0;
RECT placed{};
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 (;;) {
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<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 + 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 + 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 (result != WAIT_OBJECT_0 || !take_chunk()) {
continue;
}
// If the client has sent more frames meanwhile (drawing fell behind), catch up: carry out the older ones'
// changes without drawing them, and draw only the newest. (An incomplete frame stays for the next round.)
std::size_t complete = frame.size();
while (WaitForSingleObject(ready, 0) == WAIT_OBJECT_0) {
bool ended = take_chunk();
while (!ended && WaitForSingleObject(ready, 1000) == WAIT_OBJECT_0) {
ended = take_chunk();
}
if (!ended) {
break;
}
scene.execute(std::span(frame).first(complete), 0, false);
frame.erase(frame.begin(), frame.begin() + static_cast<std::ptrdiff_t>(complete));
complete = frame.size();
++skipped;
}
// Follow the client's 3D view's size.
const 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");
DestroyWindow(window);
return 0;
}