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
+286
-168
@@ -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>
|
||||
@@ -38,11 +40,11 @@ void DILIGENT_CALL_TYPE diligent_message(DEBUG_MESSAGE_SEVERITY severity, const
|
||||
{
|
||||
static constexpr const char* severities[] = {"info", "warning", "error", "fatal error"};
|
||||
info("Diligent {}: {}{}{}", severities[severity < 4 ? severity : 3], function != nullptr ? function : "",
|
||||
function != nullptr ? ": " : "", message != nullptr ? message : "");
|
||||
function != nullptr ? ": " : "", message != nullptr ? message : "");
|
||||
}
|
||||
|
||||
struct Arguments {
|
||||
std::wstring channel;
|
||||
std::wstring control;
|
||||
DWORD parent = 0;
|
||||
};
|
||||
|
||||
@@ -53,8 +55,8 @@ Arguments parse_arguments()
|
||||
wchar_t** argv = CommandLineToArgvW(GetCommandLineW(), &count);
|
||||
for (int i = 1; i + 1 < count; i += 2) {
|
||||
const std::wstring_view name = argv[i];
|
||||
if (name == L"--channel") {
|
||||
arguments.channel = argv[i + 1];
|
||||
if (name == L"--control") {
|
||||
arguments.control = argv[i + 1];
|
||||
} else if (name == L"--parent") {
|
||||
arguments.parent = static_cast<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),
|
||||
.lpfnWndProc = window_procedure,
|
||||
.hInstance = GetModuleHandleW(nullptr),
|
||||
.hCursor = LoadCursorW(nullptr, IDC_ARROW),
|
||||
.lpszClassName = L"T5DERenderer"};
|
||||
RegisterClassExW(&windowClass);
|
||||
return CreateWindowExW(0, windowClass.lpszClassName, L"", WS_CHILD | WS_VISIBLE | WS_DISABLED | WS_CLIPSIBLINGS,
|
||||
0, 0, 1, 1, parent, nullptr, windowClass.hInstance, nullptr);
|
||||
static const ATOM windowClass = [] {
|
||||
const WNDCLASSEXW description{.cbSize = sizeof(WNDCLASSEXW),
|
||||
.lpfnWndProc = window_procedure,
|
||||
.hInstance = GetModuleHandleW(nullptr),
|
||||
.hCursor = LoadCursorW(nullptr, IDC_ARROW),
|
||||
.lpszClassName = L"T5DERenderer"};
|
||||
return RegisterClassExW(&description);
|
||||
}();
|
||||
return CreateWindowExW(0, MAKEINTATOM(windowClass), L"", WS_CHILD | WS_VISIBLE | WS_DISABLED | WS_CLIPSIBLINGS, 0,
|
||||
0, 1, 1, parent, nullptr, GetModuleHandleW(nullptr), nullptr);
|
||||
}
|
||||
|
||||
// The view, as the adapter set it, or as %TEMP%\t5de-renderer-view.txt says while the client runs (off, split, full
|
||||
// or compare): for comparing the renderers without restarting the client. Read every second or so.
|
||||
View current_view(View adapter)
|
||||
{
|
||||
static View view = adapter;
|
||||
static std::optional<View> view;
|
||||
static ULONGLONG checked = 0;
|
||||
if (GetTickCount64() - checked < 1000) {
|
||||
return view;
|
||||
if (GetTickCount64() - checked >= 1000) {
|
||||
checked = GetTickCount64();
|
||||
view.reset();
|
||||
wchar_t temp[MAX_PATH + 1];
|
||||
const DWORD length = GetTempPathW(MAX_PATH + 1, temp);
|
||||
const HANDLE file = length == 0 || length > MAX_PATH
|
||||
? INVALID_HANDLE_VALUE
|
||||
: CreateFileW((std::filesystem::path(temp) / L"t5de-renderer-view.txt").c_str(),
|
||||
GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (file != INVALID_HANDLE_VALUE) {
|
||||
char text[16] = {};
|
||||
DWORD read = 0;
|
||||
ReadFile(file, text, sizeof(text) - 1, &read, nullptr);
|
||||
CloseHandle(file);
|
||||
const std::string_view setting(text, std::strcspn(text, "\r\n "));
|
||||
if (setting == "off") {
|
||||
view = View::Off;
|
||||
} else if (setting == "split") {
|
||||
view = View::Split;
|
||||
} else if (setting == "full") {
|
||||
view = View::Full;
|
||||
} else if (setting == "compare") {
|
||||
view = View::Compare;
|
||||
}
|
||||
}
|
||||
}
|
||||
checked = GetTickCount64();
|
||||
view = adapter;
|
||||
wchar_t temp[MAX_PATH + 1];
|
||||
const DWORD length = GetTempPathW(MAX_PATH + 1, temp);
|
||||
if (length == 0 || length > MAX_PATH) {
|
||||
return view;
|
||||
}
|
||||
const HANDLE file = CreateFileW((std::filesystem::path(temp) / L"t5de-renderer-view.txt").c_str(), GENERIC_READ,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (file == INVALID_HANDLE_VALUE) {
|
||||
return view;
|
||||
}
|
||||
char text[16] = {};
|
||||
DWORD read = 0;
|
||||
ReadFile(file, text, sizeof(text) - 1, &read, nullptr);
|
||||
CloseHandle(file);
|
||||
const std::string_view setting(text, std::strcspn(text, "\r\n "));
|
||||
view = setting == "off" ? View::Off
|
||||
: setting == "split" ? View::Split
|
||||
: setting == "full" ? View::Full
|
||||
: setting == "compare" ? View::Compare
|
||||
: adapter;
|
||||
return view;
|
||||
return view.value_or(adapter);
|
||||
}
|
||||
|
||||
// Shader changes to try while the client runs: %TEMP%\t5de-renderer-shaders\fixed_function.hlsl, if there is
|
||||
// one, replaces the built-in shaders, and is read again when it changes (checked every second or so).
|
||||
void reload_shaders(t5de::Scene& scene)
|
||||
// one, replaces the built-in shaders, and is read again when it changes (checked every second or so). Returns the
|
||||
// new source when it has changed.
|
||||
std::optional<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 || !take_chunk()) {
|
||||
continue;
|
||||
if (result >= WAIT_OBJECT_0 + 2 && result < WAIT_OBJECT_0 + handles.size()) {
|
||||
const std::size_t index = (first + (result - WAIT_OBJECT_0 - 2)) % views.size();
|
||||
views[index]->onReady();
|
||||
first = (index + 1) % views.size();
|
||||
}
|
||||
|
||||
// If the client has sent more frames meanwhile (drawing fell behind), catch up: carry out the older ones'
|
||||
// changes without drawing them, and draw only the newest. (An incomplete frame stays for the next round.)
|
||||
std::size_t complete = frame.size();
|
||||
while (WaitForSingleObject(ready, 0) == WAIT_OBJECT_0) {
|
||||
bool ended = take_chunk();
|
||||
while (!ended && WaitForSingleObject(ready, 1000) == WAIT_OBJECT_0) {
|
||||
ended = take_chunk();
|
||||
}
|
||||
if (!ended) {
|
||||
break;
|
||||
}
|
||||
scene.execute(std::span(frame).first(complete), 0, false);
|
||||
frame.erase(frame.begin(), frame.begin() + static_cast<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