CI / build (push) Failing after 0s
The full view is now the default. While the renderer process covers the whole view (Channel::covering, set after each frame it presents), the adapter skips the client's renderer's clears and draws, decided per frame; the client's renderer still gets state and resources, and draws again as soon as the renderer process stops covering the view. The client's window no longer gets WS_CLIPCHILDREN: the client draws when its window gets WM_PAINT, which a window entirely covered by clipped children never gets (it fell to about one frame a second). The flip model swap chain shows above the client's drawing anyway. Co-Authored-By: Claude Opus 5.5 <[email protected]>
326 lines
13 KiB
C++
326 lines
13 KiB
C++
// 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).
|
|
|
|
#include <windows.h>
|
|
|
|
#include <d3d11.h>
|
|
#include <dxgi.h>
|
|
#include <shellapi.h>
|
|
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <format>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
#include "EngineFactoryD3D11.h"
|
|
#include "RefCntAutoPtr.hpp"
|
|
#include "RenderDeviceD3D11.h"
|
|
#include "log.hpp"
|
|
#include "scene.hpp"
|
|
#include "t5de/protocol/channel.hpp"
|
|
|
|
namespace {
|
|
|
|
using namespace Diligent;
|
|
using t5de::protocol::Channel;
|
|
using t5de::protocol::View;
|
|
|
|
using t5de::log::info;
|
|
|
|
// Diligent's messages (shader compilation errors, invalid states, ...), into the log.
|
|
void DILIGENT_CALL_TYPE diligent_message(DEBUG_MESSAGE_SEVERITY severity, const Char* message, const Char* function,
|
|
const Char*, int)
|
|
{
|
|
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 : "");
|
|
}
|
|
|
|
struct Arguments {
|
|
std::wstring channel;
|
|
DWORD parent = 0;
|
|
};
|
|
|
|
Arguments parse_arguments()
|
|
{
|
|
Arguments arguments;
|
|
int count = 0;
|
|
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];
|
|
} else if (name == L"--parent") {
|
|
arguments.parent = static_cast<DWORD>(std::wcstoul(argv[i + 1], nullptr, 10));
|
|
}
|
|
}
|
|
LocalFree(argv);
|
|
return arguments;
|
|
}
|
|
|
|
LRESULT CALLBACK window_procedure(HWND window, UINT message, WPARAM wparam, LPARAM lparam)
|
|
{
|
|
if (message == WM_ERASEBKGND) {
|
|
return 1; // we draw all of it
|
|
}
|
|
return DefWindowProcW(window, message, wparam, lparam);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 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 ULONGLONG checked = 0;
|
|
if (GetTickCount64() - checked < 1000) {
|
|
return view;
|
|
}
|
|
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;
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
static ULONGLONG checked = 0;
|
|
static std::filesystem::file_time_type loaded{};
|
|
if (GetTickCount64() - checked < 1000) {
|
|
return;
|
|
}
|
|
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;
|
|
}
|
|
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;
|
|
}
|
|
std::string source(static_cast<std::size_t>(std::filesystem::file_size(path, error)), ' ');
|
|
DWORD read = 0;
|
|
ReadFile(file, source.data(), static_cast<DWORD>(source.size()), &read, nullptr);
|
|
CloseHandle(file);
|
|
source.resize(read);
|
|
loaded = modified;
|
|
scene.setShaderSource(std::move(source));
|
|
info("shaders reloaded from {}", path.string());
|
|
}
|
|
|
|
// Where the child window goes in the client's 3D view, and where in the 3D view its scene starts (offset).
|
|
struct Placement {
|
|
RECT window{};
|
|
int offset = 0;
|
|
};
|
|
|
|
Placement placement(HWND parent, View view)
|
|
{
|
|
Placement place;
|
|
GetClientRect(parent, &place.window);
|
|
if (view == View::Split || view == View::Compare) {
|
|
place.window.left = place.window.right / 2;
|
|
}
|
|
place.offset = view == View::Compare ? 0 : place.window.left;
|
|
if (view == View::Off) {
|
|
place.window = {};
|
|
}
|
|
return place;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
|
|
{
|
|
const Arguments arguments = parse_arguments();
|
|
if (arguments.channel.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 parent = OpenProcess(SYNCHRONIZE, FALSE, arguments.parent);
|
|
if (channel == nullptr || ready == nullptr || consumed == nullptr || parent == nullptr
|
|
|| channel->version != t5de::protocol::version) {
|
|
return 3;
|
|
}
|
|
const auto* data = reinterpret_cast<const 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));
|
|
IEngineFactoryD3D11* factory = GetEngineFactoryD3D11();
|
|
factory->SetMessageCallback(diligent_message);
|
|
RefCntAutoPtr<IRenderDevice> device;
|
|
RefCntAutoPtr<IDeviceContext> context;
|
|
EngineD3D11CreateInfo engineInfo;
|
|
factory->CreateDeviceAndContextsD3D11(engineInfo, &device, &context);
|
|
if (!device) {
|
|
info("can't create a Direct3D 11 device");
|
|
return 5;
|
|
}
|
|
// Show each frame as soon as it's drawn: Direct3D 11 otherwise lets 3 frames queue, which the view would trail by.
|
|
if (RefCntAutoPtr<IRenderDeviceD3D11> d3d11{device, IID_RenderDeviceD3D11}) {
|
|
IDXGIDevice1* dxgi = nullptr;
|
|
if (SUCCEEDED(d3d11->GetD3D11Device()->QueryInterface(__uuidof(IDXGIDevice1), reinterpret_cast<void**>(&dxgi)))) {
|
|
dxgi->SetMaximumFrameLatency(1);
|
|
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);
|
|
|
|
// 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{};
|
|
for (;;) {
|
|
const HANDLE handles[] = {ready, parent};
|
|
const DWORD result = MsgWaitForMultipleObjects(2, handles, 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 || !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;
|
|
info("exiting");
|
|
DestroyWindow(window);
|
|
return 0;
|
|
}
|