Files
t5de-renderer/renderer/main.cpp
T
dhkatzandClaude Opus 5.5 2d9da249fa
CI / build (push) Failing after 0s
Draw the whole 3D view in place of the client's renderer
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]>
2026-10-28 18:14:17 -07:00

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;
}