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:
1 parent
6e4fe589e5
commit
8055099920
5 files changed
+393
-184
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user