Draw the whole 3D view in place of the client's renderer
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]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-28 18:14:17 -07:00
1 parent 64e152de3c
commit 2d9da249fa
9 files changed
+69 -22

No files matched your search

+17
View File
@@ -47,7 +47,9 @@ void Engine::enable2dFrameBuffer() { inner_->enable2dFrameBuffer(); }
void Engine::clear(std::uint32_t flags, imvu::Color color) void Engine::clear(std::uint32_t flags, imvu::Color color)
{ {
if (innerDraws()) {
inner_->clear(flags, color); inner_->clear(flags, color);
}
recorder_->add(Op::Clear, protocol::Clear{flags, color}); recorder_->add(Op::Clear, protocol::Clear{flags, color});
} }
@@ -242,6 +244,13 @@ imvu::SharedPtr<imvu::PiTextureRenderTarget> Engine::newTextureRenderTarget(imvu
void Engine::beginRendering(imvu::PiRenderTarget* target) void Engine::beginRendering(imvu::PiRenderTarget* target)
{ {
inner_->beginRendering(target); inner_->beginRendering(target);
// Decided per frame: frames into render targets, and frames while the renderer process doesn't cover the view, are
// drawn by the client's renderer.
const bool takenOver = target == nullptr && recorder_->covering();
if (takenOver != takenOver_ && target == nullptr) {
log::info("engine: frame {}: {}", frames_, takenOver ? "drawn by the renderer process" : "drawn by the client's renderer");
}
takenOver_ = takenOver;
if (target != nullptr && foreignLogged_ < 20) { if (target != nullptr && foreignLogged_ < 20) {
++foreignLogged_; ++foreignLogged_;
log::info("engine: frame {}: drawing into a render target ({})", frames_, static_cast<void*>(target)); log::info("engine: frame {}: drawing into a render target ({})", frames_, static_cast<void*>(target));
@@ -271,7 +280,9 @@ void Engine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInterna
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords, imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
imvu::PiInternalVertexBuffer* colors) imvu::PiInternalVertexBuffer* colors)
{ {
if (innerDraws()) {
inner_->drawTriangles(inner(indices), inner(positions), inner(normals), inner(texCoords), inner(colors)); inner_->drawTriangles(inner(indices), inner(positions), inner(normals), inner(texCoords), inner(colors));
}
++(id(indices) != 0 && id(positions) != 0 ? stats_.triangles : stats_.foreignTriangles); ++(id(indices) != 0 && id(positions) != 0 ? stats_.triangles : stats_.foreignTriangles);
recorder_->add(Op::DrawTriangles, recorder_->add(Op::DrawTriangles,
protocol::DrawTriangles{id(indices), id(positions), id(normals), id(texCoords), id(colors)}); protocol::DrawTriangles{id(indices), id(positions), id(normals), id(texCoords), id(colors)});
@@ -279,13 +290,17 @@ void Engine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInterna
void Engine::drawLine(imvu::Vec3f from, imvu::Vec3f to, imvu::Color color) void Engine::drawLine(imvu::Vec3f from, imvu::Vec3f to, imvu::Color color)
{ {
if (innerDraws()) {
inner_->drawLine(from, to, color); inner_->drawLine(from, to, color);
}
++stats_.lines; ++stats_.lines;
} }
void Engine::drawPointSprites(const imvu::PointSpriteVector& sprites) void Engine::drawPointSprites(const imvu::PointSpriteVector& sprites)
{ {
if (innerDraws()) {
inner_->drawPointSprites(sprites); inner_->drawPointSprites(sprites);
}
++stats_.sprites; ++stats_.sprites;
if (sprites.first == nullptr || sprites.last <= sprites.first) { if (sprites.first == nullptr || sprites.last <= sprites.first) {
return; return;
@@ -304,7 +319,9 @@ void Engine::drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t in
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords, const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
const imvu::VertexAttributeArray<imvu::Color>& colors) const imvu::VertexAttributeArray<imvu::Color>& colors)
{ {
if (innerDraws()) {
inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, colors); inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, colors);
}
++(indices != nullptr ? stats_.indexed : stats_.unindexed); ++(indices != nullptr ? stats_.indexed : stats_.unindexed);
if (positions.data == nullptr || vertexCount == 0) { if (positions.data == nullptr || vertexCount == 0) {
return; return;
+9 -3
View File
@@ -1,7 +1,10 @@
#pragma once #pragma once
// The adapter's engine: what the client draws with. Everything goes to the client's own Direct3D renderer, which // The adapter's engine: what the client draws with. Everything is recorded for the renderer process, which draws the
// still draws the whole 3D view, and is recorded for the renderer process, which draws its part on top (the right // 3D view. The client's own Direct3D renderer gets everything too (state and resources: it still draws into render
// half for now, to compare). If the renderer process fails, the client's renderer carries on alone. // targets and reads textures back, which the renderer process doesn't yet), but while the renderer process covers the
// whole view, the client's renderer skips its frames' clears and draws. When the renderer process doesn't cover it
// (it failed, or shows only part of the view, for comparing) the client's renderer draws everything, from the next
// frame on.
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
@@ -63,10 +66,13 @@ public:
private: private:
void setState(protocol::State state, std::uint32_t value); void setState(protocol::State state, std::uint32_t value);
// Whether the client's renderer should draw this frame's clears and draws.
[[nodiscard]] bool innerDraws() const { return !takenOver_; }
imvu::SharedPtr<imvu::PiRenderEngine> inner_; imvu::SharedPtr<imvu::PiRenderEngine> inner_;
std::shared_ptr<Recorder> recorder_; std::shared_ptr<Recorder> recorder_;
std::uint64_t frames_ = 0; std::uint64_t frames_ = 0;
bool takenOver_ = false; // this frame is drawn by the renderer process only
int foreignLogged_ = 0; // diagnostics: textures the client renderer made, render targets int foreignLogged_ = 0; // diagnostics: textures the client renderer made, render targets
// Diagnostics: what the client draws, summed over 300 frames and logged (the first few times). // Diagnostics: what the client draws, summed over 300 frames and logged (the first few times).
+3 -3
View File
@@ -33,7 +33,7 @@ imvu::SharedPtr<imvu::PiRenderEngine> create_imvu_engine(imvu::SharedPtr<imvu::I
return create(std::move(window), imvu::RendererType::Direct3D); return create(std::move(window), imvu::RendererType::Direct3D);
} }
// What the renderer process draws: T5DE_RENDERER_VIEW=off, split (the default: the right half), full or compare. // What the renderer process draws: T5DE_RENDERER_VIEW=off, split (the right half), full (the default) or compare.
// (The renderer process can also be switched while it runs: renderer/main.cpp.) // (The renderer process can also be switched while it runs: renderer/main.cpp.)
protocol::View view() protocol::View view()
{ {
@@ -41,9 +41,9 @@ protocol::View view()
GetEnvironmentVariableW(L"T5DE_RENDERER_VIEW", value, 16); GetEnvironmentVariableW(L"T5DE_RENDERER_VIEW", value, 16);
const std::wstring_view setting = value; const std::wstring_view setting = value;
return setting == L"off" ? protocol::View::Off return setting == L"off" ? protocol::View::Off
: setting == L"full" ? protocol::View::Full : setting == L"split" ? protocol::View::Split
: setting == L"compare" ? protocol::View::Compare : setting == L"compare" ? protocol::View::Compare
: protocol::View::Split; : protocol::View::Full;
} }
} // namespace } // namespace
+3
View File
@@ -18,6 +18,9 @@ public:
[[nodiscard]] bool active() const { return process_ && process_->alive(); } [[nodiscard]] bool active() const { return process_ && process_->alive(); }
// Whether the renderer process draws the whole 3D view, so the client's renderer needn't.
[[nodiscard]] bool covering() const { return process_ && process_->covering(); }
// Records a command (if active); returns its payload to fill, or null. // Records a command (if active); returns its payload to fill, or null.
template <class Body> template <class Body>
std::byte* add(protocol::Op op, const Body& body, std::size_t payloadSize = 0) std::byte* add(protocol::Op op, const Body& body, std::size_t payloadSize = 0)
+7 -4
View File
@@ -84,11 +84,14 @@ std::unique_ptr<RendererProcess> RendererProcess::start(HWND window, protocol::V
.view = view, .view = view,
.capacity = protocol::channelCapacity, .capacity = protocol::channelCapacity,
.chunkSize = 0, .chunkSize = 0,
.chunkFlags = 0}; .chunkFlags = 0,
.covering = 0,
// The renderer's window goes on top of the client's 3D view: keep the client's drawing out of it. .reserved = 0};
SetWindowLongW(window, GWL_STYLE, GetWindowLongW(window, GWL_STYLE) | WS_CLIPCHILDREN);
// The renderer's window goes on top of the client's 3D view, as a child window. The client's window mustn't clip
// its children (WS_CLIPCHILDREN): the client draws a frame when its window gets 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.
const auto exe = running_copy(module_folder() / L"t5de-renderer.exe"); const auto exe = running_copy(module_folder() / L"t5de-renderer.exe");
std::wstring command = std::format(L"\"{}\" --channel {} --parent {}", exe.wstring(), name, GetCurrentProcessId()); std::wstring command = std::format(L"\"{}\" --channel {} --parent {}", exe.wstring(), name, GetCurrentProcessId());
STARTUPINFOW startup{.cb = sizeof(STARTUPINFOW)}; STARTUPINFOW startup{.cb = sizeof(STARTUPINFOW)};
+3
View File
@@ -27,6 +27,9 @@ public:
[[nodiscard]] bool alive() const { return alive_; } [[nodiscard]] bool alive() const { return alive_; }
// Whether the renderer process covers the whole 3D view (protocol::Channel::covering).
[[nodiscard]] bool covering() const { return alive_ && channel_->covering != 0; }
private: private:
RendererProcess() = default; RendererProcess() = default;
+15 -9
View File
@@ -29,19 +29,25 @@ creates (each 3D view, and again on room changes), the adapter:
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
is disabled so mouse input goes to the client's window under it, and the adapter gives the client's window is disabled so mouse input goes to the client's window under it. Its swap chain is a flip model one, which the
`WS_CLIPCHILDREN` so the client's own drawing stays out of it. The process exits when the engine is destroyed or the compositor shows above the client's own drawing. The client's window must not clip its children (`WS_CLIPCHILDREN`):
client exits. the client draws a frame when its window gets `WM_PAINT` (`SceneWindow.dll` invalidates it, and draws on the paint),
and a window entirely covered by clipped children never gets one, so the client fell to about one frame a second.
The process exits when the engine is destroyed or the client exits.
## Status ## Status
The renderer process draws the client's whole scene, over the right half of the 3D view, beside the client's own The renderer process draws the whole 3D view in place of the client's renderer (the full view, the default). The
Direct3D renderer on the left (the adapter still forwards everything to it). Checked in rooms with screenshots of adapter still forwards everything to the client's Direct3D renderer (state and resources: it draws render targets and
both renderers side by side (the compare view): they match, particles and the 2D overlay (name tags) included. reads textures back, which the renderer process doesn't yet), but while the renderer process covers the whole view
(`Channel::covering`, set after each frame it presents) the client's renderer skips its clears and draws, decided per
frame; it draws again as soon as the renderer process stops covering the view (the view changes, or the process
fails). Checked in rooms with screenshots of both renderers side by side (the compare view): they match, particles
and the 2D overlay (name tags) included.
For comparing while the client runs (development): For comparing while the client runs (development):
- Views: `T5DE_RENDERER_VIEW` (off, split, full or compare) when the client starts, or - Views: `T5DE_RENDERER_VIEW` (off, split, full (the default) or compare) when the client starts, or
`%TEMP%\t5de-renderer-view.txt` holding one of those at any time. Compare draws the left half's scene over the `%TEMP%\t5de-renderer-view.txt` holding one of those at any time. Compare draws the left half's scene over the
right half, so both renderers show the same thing side by side. right half, so both renderers show the same thing side by side.
- `%TEMP%\t5de-renderer-shaders\fixed_function.hlsl`, if present, replaces the built-in shaders and is reloaded - `%TEMP%\t5de-renderer-shaders\fixed_function.hlsl`, if present, replaces the built-in shaders and is reloaded
@@ -63,8 +69,8 @@ For comparing while the client runs (development):
the right half visibly trails, mostly from compositing another process's window over Remote Desktop. The the right half visibly trails, mostly from compositing another process's window over Remote Desktop. The
renderer process draws only the newest frame when it falls behind, and keeps at most one frame queued. renderer process draws only the newest frame when it falls behind, and keeps at most one frame queued.
Next: render targets (offscreen frames are skipped), the renderer process drawing the whole view in place of the Next: render targets (offscreen frames are skipped, and drawn by the client's renderer), and one renderer process for
client's renderer, and one renderer process for all of a client's views. all of a client's views.
## Building ## Building
+8 -3
View File
@@ -7,9 +7,10 @@
namespace t5de::protocol { namespace t5de::protocol {
inline constexpr std::uint32_t version = 2; inline constexpr std::uint32_t version = 3;
// Which part of the client's 3D view the renderer process draws (the client's own renderer draws all of it, under). // 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.
enum class View : std::uint32_t { enum class View : std::uint32_t {
Off, // none Off, // none
Split, // the right half, to compare with the client's renderer on the left Split, // the right half, to compare with the client's renderer on the left
@@ -30,9 +31,13 @@ struct Channel {
std::uint32_t capacity; std::uint32_t capacity;
std::uint32_t chunkSize; std::uint32_t chunkSize;
std::uint32_t chunkFlags; // chunkEndsFrame std::uint32_t chunkFlags; // chunkEndsFrame
// Set by the renderer process while its window covers the whole 3D view and it's presenting frames: the client's
// renderer then needn't draw (the adapter skips its draws; it draws again as soon as this is cleared).
std::uint32_t covering;
std::uint32_t reserved;
}; };
static_assert(sizeof(Channel) == 40, "same layout in 32-bit and 64-bit builds"); static_assert(sizeof(Channel) == 48, "same layout in 32-bit and 64-bit builds");
inline constexpr std::uint32_t chunkEndsFrame = 1; inline constexpr std::uint32_t chunkEndsFrame = 1;
inline constexpr std::uint32_t channelCapacity = 16 * 1024 * 1024; inline constexpr std::uint32_t channelCapacity = 16 * 1024 * 1024;
+4
View File
@@ -290,14 +290,17 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
info("drawing {}x{} at {} in the client's 3D view, its scene from {}", width, height, place.left, offset); info("drawing {}x{} at {} in the client's 3D view, its scene from {}", width, height, place.left, offset);
} else { } else {
ShowWindow(window, SW_HIDE); ShowWindow(window, SW_HIDE);
channel->covering = 0;
info("hidden"); info("hidden");
} }
} }
reload_shaders(scene); reload_shaders(scene);
LARGE_INTEGER start, end; LARGE_INTEGER start, end;
QueryPerformanceCounter(&start); 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)) { if (scene.execute(std::span(frame).first(complete), offset, width > 0 && height > 0)) {
swapChain->Present(0); swapChain->Present(0);
channel->covering = covers ? 1 : 0; // the client's renderer can stop drawing (or must start again)
QueryPerformanceCounter(&end); QueryPerformanceCounter(&end);
drawTime += static_cast<double>(end.QuadPart - start.QuadPart) / static_cast<double>(frequency.QuadPart); drawTime += static_cast<double>(end.QuadPart - start.QuadPart) / static_cast<double>(frequency.QuadPart);
latency += static_cast<double>(end.QuadPart - static_cast<LONGLONG>(scene.lastSentAt())) / latency += static_cast<double>(end.QuadPart - static_cast<LONGLONG>(scene.lastSentAt())) /
@@ -315,6 +318,7 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
frame.erase(frame.begin(), frame.begin() + static_cast<std::ptrdiff_t>(complete)); frame.erase(frame.begin(), frame.begin() + static_cast<std::ptrdiff_t>(complete));
} }
channel->covering = 0;
info("exiting"); info("exiting");
DestroyWindow(window); DestroyWindow(window);
return 0; return 0;