7 Commits
Author SHA1 Message Date
dhkatzandClaude Opus 5.5 a021e2caa9 Note the full view's speed on real hardware
CI / build (push) Successful in 23s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:47:29 -07:00
dhkatzandClaude Opus 5.5 7bf29f3e4c Clear WS_CLIPCHILDREN on the client's window
CI / build (push) Successful in 54s
The client creates its 3D view's window with it, so not adding it wasn't
enough: covered entirely by the renderer's window, the client's window got no
WM_PAINT and the client still drew about once a second.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:18:23 -07:00
dhkatzandClaude Opus 5.5 97b1d36466 Run CI steps whatever the runner's execution policy
CI / build (push) Successful in 39s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:15:07 -07:00
dhkatzandClaude Opus 5.5 2d9da249fa 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]>
2026-10-28 18:14:17 -07:00
dhkatzandClaude Opus 5.5 64e152de3c Send a User-Agent when publishing
CI / build (push) Successful in 29s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:00:24 -07:00
dhkatzandClaude Opus 5.5 c48c666309 Build and publish the renderer in CI
CI / build (push) Failing after 1m47s
Builds both programs and runs the tests on T5DE's Windows runner, and publishes
them like T5DE's own releases: pushes to main replace the nightly pre-release,
vX.Y.Z tags make release X.Y.Z.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 17:57:52 -07:00
dhkatzandClaude Opus 5.5 7d17cb0b1f Note the renderer's speed on real hardware
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 07:39:30 -07:00
11 changed files with 287 additions and 27 deletions

No files matched your search

+152
View File
@@ -0,0 +1,152 @@
"""Publish the renderer's programs. Run by ci.yml.
release.py version print version and channel
release.py publish <dir> publish the programs in dir: a release for a vX.Y.Z tag, else the rolling nightly
A vX.Y.Z tag is release X.Y.Z. Other builds are nightlies, "X.Y.Z-nightly.N+commit": N commits after the last
release tag (0.0.0 before the first). The nightly pre-release (tag `nightly`) is replaced on every push to main.
The same scheme as T5DE's own releases (t5de's .gitea/scripts/release.py).
"""
import hashlib
import json
import os
import re
import subprocess
import sys
import urllib.error
import urllib.request
import uuid
from pathlib import Path
ASSETS = ("t5deRenderEngine.dll", "t5de-renderer.exe")
NIGHTLY_TAG = "nightly"
USER_AGENT = "t5de-release (https://git.t5de.dev/t5de/t5de-renderer, 1)"
RELEASE_TAG = re.compile(r"v(\d+)\.(\d+)\.(\d+)")
def git(*args: str) -> str:
return subprocess.run(["git", *args], capture_output=True, text=True, check=True).stdout.strip()
def version() -> dict[str, str]:
ref_name = os.environ.get("GITHUB_REF_NAME", "")
if os.environ.get("GITHUB_REF_TYPE") == "tag" and RELEASE_TAG.fullmatch(ref_name):
return {"version": ref_name[1:], "channel": "release", "tag": ref_name}
try:
last = git("describe", "--tags", "--abbrev=0", "--match", "v[0-9]*.[0-9]*.[0-9]*")
count = int(git("rev-list", "--count", f"{last}..HEAD"))
base = last[1:]
except subprocess.CalledProcessError: # no release yet
count, base = int(git("rev-list", "--count", "HEAD")), "0.0.0"
commit = git("rev-parse", "--short=7", "HEAD")
return {"version": f"{base}-nightly.{count}+{commit}", "channel": "nightly", "tag": NIGHTLY_TAG}
class Gitea:
def __init__(self):
self.api = f"{os.environ['GITHUB_SERVER_URL']}/api/v1/repos/{os.environ['GITHUB_REPOSITORY']}"
self.token = os.environ["GITEA_TOKEN"]
def request(self, method: str, path: str, body: dict | None = None, data: bytes | None = None,
content_type: str = "application/json"):
if body is not None:
data = json.dumps(body).encode()
req = urllib.request.Request(self.api + path, method=method, data=data,
# Cloudflare in front of the server refuses Python's default User-Agent.
headers={"Authorization": f"token {self.token}", "Content-Type": content_type,
"User-Agent": USER_AGENT})
try:
with urllib.request.urlopen(req, timeout=120) as response:
text = response.read()
return json.loads(text) if text else None
except urllib.error.HTTPError as e:
if e.code == 404 and method in ("GET", "DELETE"):
return None
raise SystemExit(f"{method} {path}: {e.code} {e.read().decode(errors='replace')}")
def upload(self, release_id: int, name: str, content: bytes):
boundary = uuid.uuid4().hex
data = (f"--{boundary}\r\nContent-Disposition: form-data; name=\"attachment\"; filename=\"{name}\"\r\n"
f"Content-Type: application/octet-stream\r\n\r\n").encode() + content + f"\r\n--{boundary}--\r\n".encode()
return self.request("POST", f"/releases/{release_id}/assets?name={name}", data=data,
content_type=f"multipart/form-data; boundary={boundary}")
def notes(info: dict[str, str], files: dict[str, bytes]) -> str:
nightly = ("A nightly build of the latest commit, for testing; it's replaced by the next one.\n\n"
if info["channel"] == "nightly" else "")
hashes = "\n".join(f"`{name}` sha256: `{hashlib.sha256(content).hexdigest()}`" for name, content in files.items())
return f"""{nightly}T5DE's renderer for IMVU Classic: copy both files into a T5DE install's folder, and choose OpenGL
in the client's Settings > Advanced (with T5DE's renderer patch).
Unofficial: not made by or affiliated with IMVU. Use at your own risk.
Version {info['version']} · commit {os.environ.get('GITHUB_SHA', git('rev-parse', 'HEAD'))}
{hashes}
"""
def publish(folder: str):
info = version()
files = {name: (Path(folder) / name).read_bytes() for name in ASSETS}
gitea = Gitea()
previous = None
if info["channel"] == "nightly":
old = gitea.request("GET", f"/releases/tags/{NIGHTLY_TAG}")
if old:
previous = old["target_commitish"]
gitea.request("DELETE", f"/releases/{old['id']}")
gitea.request("DELETE", f"/tags/{NIGHTLY_TAG}")
release = gitea.request("POST", "/releases", {
"tag_name": NIGHTLY_TAG, "target_commitish": os.environ["GITHUB_SHA"],
"name": f"Nightly {info['version']}", "body": notes(info, files), "prerelease": True})
else:
release = gitea.request("GET", f"/releases/tags/{info['tag']}")
fields = {"name": f"T5DE Renderer {info['version']}", "body": notes(info, files), "prerelease": False}
if release: # a rerun: refresh it
release = gitea.request("PATCH", f"/releases/{release['id']}", fields)
for asset in release.get("assets") or []:
if asset["name"] in files:
gitea.request("DELETE", f"/releases/{release['id']}/assets/{asset['id']}")
else:
release = gitea.request("POST", "/releases", {"tag_name": info["tag"], **fields})
for name, content in files.items():
gitea.upload(release["id"], name, content)
print(f"Published {release['html_url']}")
if info["channel"] == "nightly" and os.environ.get("DISCORD_NIGHTLY_WEBHOOK"):
announce(os.environ["DISCORD_NIGHTLY_WEBHOOK"], info, release, previous)
def announce(webhook: str, info: dict[str, str], release: dict, previous: str | None):
"""Post the release to a Discord channel. Failing to doesn't fail the release."""
commits = []
if previous:
try:
commits = git("log", "--format=%h %s", "--no-merges", f"{previous}..HEAD").splitlines()
except subprocess.CalledProcessError: # the previous nightly's commit is gone (history rewritten)
pass
shown = commits[:10] + ([f"and {len(commits) - 10} more"] if len(commits) > 10 else [])
description = f"[Download]({release['html_url']})"
if shown:
description += "\n\n" + "\n".join(f"• {line}" for line in shown)
body = {"embeds": [{"title": f"Renderer Nightly {info['version']}", "url": release["html_url"],
"description": description[:4000], "color": 0xFFD200}]}
req = urllib.request.Request(webhook, method="POST", data=json.dumps(body).encode(),
headers={"Content-Type": "application/json",
"User-Agent": USER_AGENT})
try:
urllib.request.urlopen(req, timeout=30).close()
print("Announced on Discord")
except (urllib.error.URLError, TimeoutError) as e:
print(f"warning: couldn't post to Discord: {e}")
if __name__ == "__main__":
if sys.argv[1:] == ["version"]:
for key, value in version().items():
print(f"{key}={value}")
elif len(sys.argv) == 3 and sys.argv[1] == "publish":
publish(sys.argv[2])
else:
raise SystemExit(__doc__)
+59
View File
@@ -0,0 +1,59 @@
# Builds the adapter (32-bit) and the renderer process (64-bit), runs the tests, and publishes both programs: pushes to
# main replace the nightly pre-release, vX.Y.Z tags make release X.Y.Z (.gitea/scripts/release.py).
#
# Runs on T5DE's Windows runner (label `windows`, jobs run directly on the machine: it needs Visual Studio's
# compiler). The checkout lives in a fixed folder there ($T5DE_BUILD_ROOT, set in the runner's config), so Diligent
# Engine is fetched and built once, not on every run. Never on pull requests: their code would run on that machine.
name: CI
on:
push:
branches: [main]
tags: ["v*"]
workflow_dispatch:
jobs:
build:
runs-on: windows
defaults:
run:
# Steps run as script files, which Windows' default execution policy refuses.
shell: powershell -NoProfile -ExecutionPolicy Bypass -Command ". '{0}'"
steps:
- name: Check out
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
function git { & git.exe -c "http.extraHeader=Authorization: token $env:TOKEN" @args; if ($LASTEXITCODE) { exit $LASTEXITCODE } }
$root = if ($env:T5DE_BUILD_ROOT) { $env:T5DE_BUILD_ROOT } else { $env:GITHUB_WORKSPACE }
$src = Join-Path $root ($env:GITHUB_REPOSITORY -replace '/', '-')
if (-not (Test-Path (Join-Path $src .git))) { git init -q $src }
Set-Location $src
git fetch -q --force --tags "$env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY" "+${env:GITHUB_REF}:refs/remotes/ci/head"
git checkout -q --force --detach $env:GITHUB_SHA
git clean -q -ffdx -e /build/
"T5DE_SRC=$src" | Out-File -Append -Encoding utf8 $env:GITHUB_ENV
- name: Build and test the adapter
working-directory: ${{ env.T5DE_SRC }}
run: cmake --workflow --preset adapter
- name: Build the renderer process
working-directory: ${{ env.T5DE_SRC }}
run: cmake --workflow --preset renderer
- name: Collect the programs
working-directory: ${{ env.T5DE_SRC }}
run: |
cmake --install build/windows-x86 --config Release --prefix dist
if ($LASTEXITCODE) { exit $LASTEXITCODE }
cmake --install build/windows-x64 --config Release --prefix dist
- name: Publish the release
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
working-directory: ${{ env.T5DE_SRC }}
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Optional: a Discord webhook URL that nightlies are announced on.
DISCORD_NIGHTLY_WEBHOOK: ${{ secrets.DISCORD_NIGHTLY_WEBHOOK }}
run: python .gitea/scripts/release.py publish dist
+17
View File
@@ -47,7 +47,9 @@ void Engine::enable2dFrameBuffer() { inner_->enable2dFrameBuffer(); }
void Engine::clear(std::uint32_t flags, imvu::Color color)
{
if (innerDraws()) {
inner_->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)
{
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) {
++foreignLogged_;
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* colors)
{
if (innerDraws()) {
inner_->drawTriangles(inner(indices), inner(positions), inner(normals), inner(texCoords), inner(colors));
}
++(id(indices) != 0 && id(positions) != 0 ? stats_.triangles : stats_.foreignTriangles);
recorder_->add(Op::DrawTriangles,
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)
{
if (innerDraws()) {
inner_->drawLine(from, to, color);
}
++stats_.lines;
}
void Engine::drawPointSprites(const imvu::PointSpriteVector& sprites)
{
if (innerDraws()) {
inner_->drawPointSprites(sprites);
}
++stats_.sprites;
if (sprites.first == nullptr || sprites.last <= sprites.first) {
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::Color>& colors)
{
if (innerDraws()) {
inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, colors);
}
++(indices != nullptr ? stats_.indexed : stats_.unindexed);
if (positions.data == nullptr || vertexCount == 0) {
return;
+9 -3
View File
@@ -1,7 +1,10 @@
#pragma once
// The adapter's engine: what the client draws with. Everything goes to the client's own Direct3D renderer, which
// still draws the whole 3D view, and is recorded for the renderer process, which draws its part on top (the right
// half for now, to compare). If the renderer process fails, the client's renderer carries on alone.
// The adapter's engine: what the client draws with. Everything is recorded for the renderer process, which draws the
// 3D view. The client's own Direct3D renderer gets everything too (state and resources: it still draws into render
// 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 <memory>
@@ -63,10 +66,13 @@ public:
private:
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_;
std::shared_ptr<Recorder> recorder_;
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
// 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);
}
// 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.)
protocol::View view()
{
@@ -41,9 +41,9 @@ protocol::View view()
GetEnvironmentVariableW(L"T5DE_RENDERER_VIEW", value, 16);
const std::wstring_view setting = value;
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
: protocol::View::Split;
: protocol::View::Full;
}
} // namespace
+3
View File
@@ -18,6 +18,9 @@ public:
[[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.
template <class Body>
std::byte* add(protocol::Op op, const Body& body, std::size_t payloadSize = 0)
+8 -4
View File
@@ -84,11 +84,15 @@ std::unique_ptr<RendererProcess> RendererProcess::start(HWND window, protocol::V
.view = view,
.capacity = protocol::channelCapacity,
.chunkSize = 0,
.chunkFlags = 0};
// The renderer's window goes on top of the client's 3D view: keep the client's drawing out of it.
SetWindowLongW(window, GWL_STYLE, GetWindowLongW(window, GWL_STYLE) | WS_CLIPCHILDREN);
.chunkFlags = 0,
.covering = 0,
.reserved = 0};
// 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, which the client creates it with): 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.
SetWindowLongW(window, GWL_STYLE, GetWindowLongW(window, GWL_STYLE) & ~WS_CLIPCHILDREN);
const auto exe = running_copy(module_folder() / L"t5de-renderer.exe");
std::wstring command = std::format(L"\"{}\" --channel {} --parent {}", exe.wstring(), name, GetCurrentProcessId());
STARTUPINFOW startup{.cb = sizeof(STARTUPINFOW)};
+3
View File
@@ -27,6 +27,9 @@ public:
[[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:
RendererProcess() = default;
+21 -14
View File
@@ -29,19 +29,26 @@ 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
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
`WS_CLIPCHILDREN` so the client's own drawing stays out of it. The process exits when the engine is destroyed or the
client exits.
is disabled so mouse input goes to the client's window under it. Its swap chain is a flip model one, which the
compositor shows above the client's own drawing. The client's window must not clip its children (`WS_CLIPCHILDREN`,
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),
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
The renderer process draws the client's whole scene, over the right half of the 3D view, beside the client's own
Direct3D renderer on the left (the adapter still forwards everything to it). Checked in rooms with screenshots of
both renderers side by side (the compare view): they match, particles and the 2D overlay (name tags) included.
The renderer process draws the whole 3D view in place of the client's renderer (the full view, the default). The
adapter still forwards everything to the client's Direct3D renderer (state and resources: it draws render targets and
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):
- 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
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
@@ -57,14 +64,14 @@ For comparing while the client runs (development):
be taken, and gives up on a renderer process that stops taking them (the client's renderer carries on).
- The renderer process reproduces IMVU's Direct3D 9 setup (`renderer/shaders/fixed_function.hlsl`), particles
included (point sprites, drawn as instanced squares), with a pipeline per combination of states.
- Speed: on the development VM (software rendering) a room frame takes 8 to 60 ms to draw; the time from the
client finishing a frame to ours being presented averages about 60 ms. The renderer process draws only the
newest frame when it falls behind, and keeps at most one frame queued. Over Remote Desktop, the half drawn by the
renderer process (another process's window, composited by Windows) visibly trails the client's half by more
than that; that delay comes after presenting, and is to be checked on a real graphics card and display.
- Speed: on a PC with a graphics card, drawing the whole view (3440x1059, a room of some 220 draws) takes 1.3 to
3.8 ms a frame, and the time from the client finishing a frame to ours being presented is the same (frames don't
queue, none skipped). Earlier, in the split view, the halves couldn't be told apart, even while the camera moved. On the development VM (software rendering, over Remote Desktop) drawing takes 8 to 60 ms, and
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.
Next: render targets (offscreen frames are skipped), the renderer process drawing the whole view in place of the
client's renderer, and one renderer process for all of a client's views.
Next: render targets (offscreen frames are skipped, and drawn by the client's renderer), and one renderer process for
all of a client's views.
## Building
+8 -3
View File
@@ -7,9 +7,10 @@
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 {
Off, // none
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 chunkSize;
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 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);
} 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())) /
@@ -315,6 +318,7 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
frame.erase(frame.begin(), frame.begin() + static_cast<std::ptrdiff_t>(complete));
}
channel->covering = 0;
info("exiting");
DestroyWindow(window);
return 0;