Compare commits
7
Commits
3cdda4c7ed
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a021e2caa9 | ||
|
|
7bf29f3e4c | ||
|
|
97b1d36466 | ||
|
|
2d9da249fa | ||
|
|
64e152de3c | ||
|
|
c48c666309 | ||
|
|
7d17cb0b1f |
No files matched your search
@@ -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__)
|
||||
@@ -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
|
||||
+22
-5
@@ -47,7 +47,9 @@ void Engine::enable2dFrameBuffer() { inner_->enable2dFrameBuffer(); }
|
||||
|
||||
void Engine::clear(std::uint32_t flags, imvu::Color color)
|
||||
{
|
||||
inner_->clear(flags, 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)
|
||||
{
|
||||
inner_->drawTriangles(inner(indices), inner(positions), inner(normals), inner(texCoords), inner(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)
|
||||
{
|
||||
inner_->drawLine(from, to, color);
|
||||
if (innerDraws()) {
|
||||
inner_->drawLine(from, to, color);
|
||||
}
|
||||
++stats_.lines;
|
||||
}
|
||||
|
||||
void Engine::drawPointSprites(const imvu::PointSpriteVector& sprites)
|
||||
{
|
||||
inner_->drawPointSprites(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)
|
||||
{
|
||||
inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, 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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)};
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user