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
+404
-195
No files matched your search
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user