Compare commits
10
Commits
a04dba18c1
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a021e2caa9 | ||
|
|
7bf29f3e4c | ||
|
|
97b1d36466 | ||
|
|
2d9da249fa | ||
|
|
64e152de3c | ||
|
|
c48c666309 | ||
|
|
7d17cb0b1f | ||
|
|
3cdda4c7ed | ||
|
|
299cd8c312 | ||
|
|
1a45bf9a86 |
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
|
||||||
+67
-6
@@ -5,6 +5,7 @@
|
|||||||
#include <bit>
|
#include <bit>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "log.hpp"
|
#include "log.hpp"
|
||||||
#include "resources.hpp"
|
#include "resources.hpp"
|
||||||
@@ -46,13 +47,18 @@ 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)
|
||||||
{
|
{
|
||||||
inner_->clear(flags, color);
|
if (innerDraws()) {
|
||||||
|
inner_->clear(flags, color);
|
||||||
|
}
|
||||||
recorder_->add(Op::Clear, protocol::Clear{flags, color});
|
recorder_->add(Op::Clear, protocol::Clear{flags, color});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Engine::setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection)
|
void Engine::setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection)
|
||||||
{
|
{
|
||||||
inner_->setProjection(viewport, projection);
|
inner_->setProjection(viewport, projection);
|
||||||
|
if (projection.m[2][3] == 0.0f) {
|
||||||
|
++stats_.flat;
|
||||||
|
}
|
||||||
protocol::SetProjection command{viewport.x, viewport.y, {}};
|
protocol::SetProjection command{viewport.x, viewport.y, {}};
|
||||||
std::memcpy(command.m, &projection.m[0][0], sizeof(command.m));
|
std::memcpy(command.m, &projection.m[0][0], sizeof(command.m));
|
||||||
recorder_->add(Op::SetProjection, command);
|
recorder_->add(Op::SetProjection, command);
|
||||||
@@ -73,6 +79,12 @@ void Engine::setViewSpace(const imvu::Transform& view)
|
|||||||
void Engine::setTextures(imvu::PiRenderTexture* texture0, imvu::PiRenderTexture* texture1)
|
void Engine::setTextures(imvu::PiRenderTexture* texture0, imvu::PiRenderTexture* texture1)
|
||||||
{
|
{
|
||||||
inner_->setTextures(inner(texture0), inner(texture1));
|
inner_->setTextures(inner(texture0), inner(texture1));
|
||||||
|
for (imvu::PiRenderTexture* texture : {texture0, texture1}) {
|
||||||
|
if (texture != nullptr && id(texture) == 0 && foreignLogged_ < 20) {
|
||||||
|
++foreignLogged_;
|
||||||
|
log::info("engine: frame {}: a texture the client renderer made ({})", frames_, static_cast<void*>(texture));
|
||||||
|
}
|
||||||
|
}
|
||||||
recorder_->add(Op::SetTextures, protocol::SetTextures{id(texture0), id(texture1)});
|
recorder_->add(Op::SetTextures, protocol::SetTextures{id(texture0), id(texture1)});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,17 +231,30 @@ imvu::PiInternalVertexBuffer* Engine::newDynamicVertexBuffer(std::uint32_t eleme
|
|||||||
|
|
||||||
imvu::SharedPtr<imvu::PiPixmapRenderTarget> Engine::newPixmapRenderTarget(imvu::Vec2i size)
|
imvu::SharedPtr<imvu::PiPixmapRenderTarget> Engine::newPixmapRenderTarget(imvu::Vec2i size)
|
||||||
{
|
{
|
||||||
|
log::info("engine: frame {}: a {}x{} pixmap render target", frames_, size.x, size.y);
|
||||||
return inner_->newPixmapRenderTarget(size);
|
return inner_->newPixmapRenderTarget(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
imvu::SharedPtr<imvu::PiTextureRenderTarget> Engine::newTextureRenderTarget(imvu::Vec2i size)
|
imvu::SharedPtr<imvu::PiTextureRenderTarget> Engine::newTextureRenderTarget(imvu::Vec2i size)
|
||||||
{
|
{
|
||||||
|
log::info("engine: frame {}: a {}x{} texture render target", frames_, size.x, size.y);
|
||||||
return inner_->newTextureRenderTarget(size);
|
return inner_->newTextureRenderTarget(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
++foreignLogged_;
|
||||||
|
log::info("engine: frame {}: drawing into a render target ({})", frames_, static_cast<void*>(target));
|
||||||
|
}
|
||||||
recorder_->add(Op::BeginFrame, protocol::BeginFrame{target != nullptr});
|
recorder_->add(Op::BeginFrame, protocol::BeginFrame{target != nullptr});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,22 +266,42 @@ void Engine::endRendering()
|
|||||||
recorder_->add(Op::EndFrame, protocol::EndFrame{static_cast<std::uint64_t>(now.QuadPart)});
|
recorder_->add(Op::EndFrame, protocol::EndFrame{static_cast<std::uint64_t>(now.QuadPart)});
|
||||||
recorder_->flush(true);
|
recorder_->flush(true);
|
||||||
++frames_;
|
++frames_;
|
||||||
|
if (frames_ % 300 == 0 && statsLogged_ < 6) {
|
||||||
|
++statsLogged_;
|
||||||
|
log::info("engine: last 300 frames: drawTriangles {} (and {} with buffers not ours), drawTrianglesFromMemory {} "
|
||||||
|
"(and {} without indices), point sprites {}, lines {}, 2D projections {}",
|
||||||
|
stats_.triangles, stats_.foreignTriangles, stats_.indexed, stats_.unindexed, stats_.sprites, stats_.lines,
|
||||||
|
stats_.flat);
|
||||||
|
stats_ = {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Engine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
|
void Engine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
|
||||||
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
|
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
|
||||||
imvu::PiInternalVertexBuffer* colors)
|
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,
|
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)});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Engine::drawLine(imvu::Vec3f from, imvu::Vec3f to, imvu::Color color) { inner_->drawLine(from, to, color); }
|
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)
|
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) {
|
if (sprites.first == nullptr || sprites.last <= sprites.first) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -274,10 +319,26 @@ 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)
|
||||||
{
|
{
|
||||||
inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, colors);
|
if (innerDraws()) {
|
||||||
if (indices == nullptr || positions.data == nullptr || vertexCount == 0) {
|
inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, colors);
|
||||||
|
}
|
||||||
|
++(indices != nullptr ? stats_.indexed : stats_.unindexed);
|
||||||
|
if (positions.data == nullptr || vertexCount == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Without indices: the vertices in order, as a triangle list.
|
||||||
|
std::vector<std::uint16_t> sequence;
|
||||||
|
if (indices == nullptr) {
|
||||||
|
if (vertexCount > 0xffff) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sequence.resize(vertexCount - vertexCount % 3);
|
||||||
|
for (std::size_t i = 0; i < sequence.size(); ++i) {
|
||||||
|
sequence[i] = static_cast<std::uint16_t>(i);
|
||||||
|
}
|
||||||
|
indices = sequence.data();
|
||||||
|
indexCount = static_cast<std::uint32_t>(sequence.size());
|
||||||
|
}
|
||||||
const bool hasNormals = normals.data != nullptr && normals.count >= vertexCount;
|
const bool hasNormals = normals.data != nullptr && normals.count >= vertexCount;
|
||||||
const bool hasTexCoords = texCoords.data != nullptr && texCoords.count >= vertexCount;
|
const bool hasTexCoords = texCoords.data != nullptr && texCoords.count >= vertexCount;
|
||||||
const bool hasColors = colors.data != nullptr && colors.count >= vertexCount;
|
const bool hasColors = colors.data != nullptr && colors.count >= vertexCount;
|
||||||
|
|||||||
+24
-3
@@ -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,28 @@ 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
|
||||||
|
|
||||||
|
// Diagnostics: what the client draws, summed over 300 frames and logged (the first few times).
|
||||||
|
struct Stats {
|
||||||
|
std::uint32_t triangles = 0; // drawTriangles with our buffers
|
||||||
|
std::uint32_t foreignTriangles = 0; // drawTriangles with buffers that aren't ours (not drawn)
|
||||||
|
std::uint32_t indexed = 0; // drawTrianglesFromMemory with indices
|
||||||
|
std::uint32_t unindexed = 0; // drawTrianglesFromMemory without
|
||||||
|
std::uint32_t sprites = 0;
|
||||||
|
std::uint32_t lines = 0;
|
||||||
|
std::uint32_t flat = 0; // setProjection with an orthographic projection (2D)
|
||||||
|
};
|
||||||
|
Stats stats_;
|
||||||
|
int statsLogged_ = 0;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace t5de
|
} // namespace t5de
|
||||||
+6
-2
@@ -33,13 +33,17 @@ 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) or full.
|
// 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()
|
protocol::View view()
|
||||||
{
|
{
|
||||||
wchar_t value[16] = {};
|
wchar_t value[16] = {};
|
||||||
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 : setting == L"full" ? protocol::View::Full : protocol::View::Split;
|
return setting == L"off" ? protocol::View::Off
|
||||||
|
: setting == L"split" ? protocol::View::Split
|
||||||
|
: setting == L"compare" ? protocol::View::Compare
|
||||||
|
: protocol::View::Full;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -29,6 +29,31 @@ std::filesystem::path module_folder()
|
|||||||
|
|
||||||
std::string narrow(const std::wstring& text) { return std::filesystem::path(text).string(); }
|
std::string narrow(const std::wstring& text) { return std::filesystem::path(text).string(); }
|
||||||
|
|
||||||
|
// The renderer process runs from a copy in %TEMP%\t5de-renderer, one per version of the program (by size and
|
||||||
|
// modification time), so the program in the client's folder can be replaced (updated) while the client runs: the
|
||||||
|
// next engine runs the new one. Copies no longer running are removed. Falls back to the program itself.
|
||||||
|
std::filesystem::path running_copy(const std::filesystem::path& exe)
|
||||||
|
{
|
||||||
|
std::error_code error;
|
||||||
|
const auto size = std::filesystem::file_size(exe, error);
|
||||||
|
const auto modified = std::filesystem::last_write_time(exe, error);
|
||||||
|
if (error) {
|
||||||
|
return exe;
|
||||||
|
}
|
||||||
|
const auto folder = std::filesystem::temp_directory_path(error) / L"t5de-renderer";
|
||||||
|
std::filesystem::create_directories(folder, error);
|
||||||
|
const auto copy = folder / std::format(L"t5de-renderer-{}-{}.exe", size, modified.time_since_epoch().count());
|
||||||
|
if (!std::filesystem::exists(copy, error)) {
|
||||||
|
for (const auto& old : std::filesystem::directory_iterator(folder, error)) {
|
||||||
|
std::filesystem::remove(old.path(), error); // fails, harmlessly, for copies still running
|
||||||
|
}
|
||||||
|
if (!CopyFileW(exe.c_str(), copy.c_str(), FALSE)) {
|
||||||
|
return exe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::unique_ptr<RendererProcess> RendererProcess::start(HWND window, protocol::View view)
|
std::unique_ptr<RendererProcess> RendererProcess::start(HWND window, protocol::View view)
|
||||||
@@ -59,12 +84,16 @@ 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,
|
||||||
|
.reserved = 0};
|
||||||
|
|
||||||
// The renderer's window goes on top of the client's 3D view: keep the client's drawing out of it.
|
// The renderer's window goes on top of the client's 3D view, as a child window. The client's window mustn't clip
|
||||||
SetWindowLongW(window, GWL_STYLE, GetWindowLongW(window, GWL_STYLE) | WS_CLIPCHILDREN);
|
// 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
|
||||||
const auto exe = module_folder() / L"t5de-renderer.exe";
|
// 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());
|
std::wstring command = std::format(L"\"{}\" --channel {} --parent {}", exe.wstring(), name, GetCurrentProcessId());
|
||||||
STARTUPINFOW startup{.cb = sizeof(STARTUPINFOW)};
|
STARTUPINFOW startup{.cb = sizeof(STARTUPINFOW)};
|
||||||
PROCESS_INFORMATION process{};
|
PROCESS_INFORMATION process{};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,56 @@
|
|||||||
#include "resources.hpp"
|
#include "resources.hpp"
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace t5de {
|
namespace t5de {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Our live resources, by address.
|
||||||
|
struct Registry {
|
||||||
|
std::mutex mutex;
|
||||||
|
std::unordered_set<const void*> objects;
|
||||||
|
};
|
||||||
|
|
||||||
|
Registry& registry()
|
||||||
|
{
|
||||||
|
static Registry instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void add(const void* object)
|
||||||
|
{
|
||||||
|
const std::scoped_lock lock(registry().mutex);
|
||||||
|
registry().objects.insert(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove(const void* object)
|
||||||
|
{
|
||||||
|
const std::scoped_lock lock(registry().mutex);
|
||||||
|
registry().objects.erase(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool ours(const void* object)
|
||||||
|
{
|
||||||
|
if (object == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const std::scoped_lock lock(registry().mutex);
|
||||||
|
return registry().objects.contains(object);
|
||||||
|
}
|
||||||
|
|
||||||
Texture::Texture(imvu::PiRenderTexture* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
|
Texture::Texture(imvu::PiRenderTexture* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
|
||||||
: inner_(inner), recorder_(std::move(recorder)), id_(id)
|
: inner_(inner), recorder_(std::move(recorder)), id_(id)
|
||||||
{
|
{
|
||||||
|
add(static_cast<imvu::PiRenderTexture*>(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
Texture::~Texture()
|
Texture::~Texture()
|
||||||
{
|
{
|
||||||
|
remove(static_cast<imvu::PiRenderTexture*>(this));
|
||||||
delete inner_; // through its virtual destructor: IMVU's code frees it
|
delete inner_; // through its virtual destructor: IMVU's code frees it
|
||||||
recorder_->destroyTexture(id_);
|
recorder_->destroyTexture(id_);
|
||||||
}
|
}
|
||||||
@@ -32,10 +74,12 @@ imvu::Pixmap* Texture::read(imvu::Pixmap* result) const { return inner_->read(re
|
|||||||
IndexBuffer::IndexBuffer(imvu::PiInternalIndexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
|
IndexBuffer::IndexBuffer(imvu::PiInternalIndexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
|
||||||
: inner_(inner), recorder_(std::move(recorder)), id_(id)
|
: inner_(inner), recorder_(std::move(recorder)), id_(id)
|
||||||
{
|
{
|
||||||
|
add(static_cast<imvu::PiInternalIndexBuffer*>(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
IndexBuffer::~IndexBuffer()
|
IndexBuffer::~IndexBuffer()
|
||||||
{
|
{
|
||||||
|
remove(static_cast<imvu::PiInternalIndexBuffer*>(this));
|
||||||
delete inner_;
|
delete inner_;
|
||||||
recorder_->destroyBuffer(id_);
|
recorder_->destroyBuffer(id_);
|
||||||
}
|
}
|
||||||
@@ -43,10 +87,12 @@ IndexBuffer::~IndexBuffer()
|
|||||||
VertexBuffer::VertexBuffer(imvu::PiInternalVertexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
|
VertexBuffer::VertexBuffer(imvu::PiInternalVertexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
|
||||||
: inner_(inner), recorder_(std::move(recorder)), id_(id)
|
: inner_(inner), recorder_(std::move(recorder)), id_(id)
|
||||||
{
|
{
|
||||||
|
add(static_cast<imvu::PiInternalVertexBuffer*>(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
VertexBuffer::~VertexBuffer()
|
VertexBuffer::~VertexBuffer()
|
||||||
{
|
{
|
||||||
|
remove(static_cast<imvu::PiInternalVertexBuffer*>(this));
|
||||||
delete inner_;
|
delete inner_;
|
||||||
recorder_->destroyBuffer(id_);
|
recorder_->destroyBuffer(id_);
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-7
@@ -62,30 +62,34 @@ private:
|
|||||||
std::uint32_t id_;
|
std::uint32_t id_;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The client renderer's object and our id for something the engine made; null and 0 for null.
|
// Whether an object is one of ours (live Texture, IndexBuffer or VertexBuffer): the client can also hand the engine
|
||||||
|
// objects the client renderer made itself (render targets' textures), which must reach it unchanged.
|
||||||
|
bool ours(const void* object);
|
||||||
|
|
||||||
|
// The client renderer's object, and our id, for what the client passes in: ours unwrapped, others unchanged (id 0).
|
||||||
inline imvu::PiRenderTexture* inner(imvu::PiRenderTexture* texture)
|
inline imvu::PiRenderTexture* inner(imvu::PiRenderTexture* texture)
|
||||||
{
|
{
|
||||||
return texture != nullptr ? static_cast<Texture*>(texture)->inner() : nullptr;
|
return ours(texture) ? static_cast<Texture*>(texture)->inner() : texture;
|
||||||
}
|
}
|
||||||
inline std::uint32_t id(imvu::PiRenderTexture* texture)
|
inline std::uint32_t id(imvu::PiRenderTexture* texture)
|
||||||
{
|
{
|
||||||
return texture != nullptr ? static_cast<Texture*>(texture)->id() : 0;
|
return ours(texture) ? static_cast<Texture*>(texture)->id() : 0;
|
||||||
}
|
}
|
||||||
inline imvu::PiInternalIndexBuffer* inner(imvu::PiInternalIndexBuffer* buffer)
|
inline imvu::PiInternalIndexBuffer* inner(imvu::PiInternalIndexBuffer* buffer)
|
||||||
{
|
{
|
||||||
return buffer != nullptr ? static_cast<IndexBuffer*>(buffer)->inner() : nullptr;
|
return ours(buffer) ? static_cast<IndexBuffer*>(buffer)->inner() : buffer;
|
||||||
}
|
}
|
||||||
inline std::uint32_t id(imvu::PiInternalIndexBuffer* buffer)
|
inline std::uint32_t id(imvu::PiInternalIndexBuffer* buffer)
|
||||||
{
|
{
|
||||||
return buffer != nullptr ? static_cast<IndexBuffer*>(buffer)->id() : 0;
|
return ours(buffer) ? static_cast<IndexBuffer*>(buffer)->id() : 0;
|
||||||
}
|
}
|
||||||
inline imvu::PiInternalVertexBuffer* inner(imvu::PiInternalVertexBuffer* buffer)
|
inline imvu::PiInternalVertexBuffer* inner(imvu::PiInternalVertexBuffer* buffer)
|
||||||
{
|
{
|
||||||
return buffer != nullptr ? static_cast<VertexBuffer*>(buffer)->inner() : nullptr;
|
return ours(buffer) ? static_cast<VertexBuffer*>(buffer)->inner() : buffer;
|
||||||
}
|
}
|
||||||
inline std::uint32_t id(imvu::PiInternalVertexBuffer* buffer)
|
inline std::uint32_t id(imvu::PiInternalVertexBuffer* buffer)
|
||||||
{
|
{
|
||||||
return buffer != nullptr ? static_cast<VertexBuffer*>(buffer)->id() : 0;
|
return ours(buffer) ? static_cast<VertexBuffer*>(buffer)->id() : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace t5de
|
} // namespace t5de
|
||||||
+33
-13
@@ -29,29 +29,49 @@ 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.
|
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
|
## 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): `T5DE_RENDERER_VIEW=full` or `off`
|
adapter still forwards everything to the client's Direct3D renderer (state and resources: it draws render targets and
|
||||||
changes that. Checked in rooms: the halves match, particles 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):
|
||||||
|
|
||||||
|
- 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
|
||||||
|
when it changes.
|
||||||
|
- The renderer process runs from a copy (`%TEMP%\t5de-renderer`), so `t5de-renderer.exe` can be replaced while the
|
||||||
|
client runs; engines started afterwards run the new one (the room's view keeps its engine until the client
|
||||||
|
restarts).
|
||||||
|
- The adapter's log (`%TEMP%\t5de-renderer.log`) sums up what the client draws every 300 frames (the first few
|
||||||
|
times), and logs textures and render targets the client renderer made itself.
|
||||||
|
|
||||||
- The command stream (`include/t5de/protocol/commands.hpp`) carries state, transforms, lights, fog, textures,
|
- The command stream (`include/t5de/protocol/commands.hpp`) carries state, transforms, lights, fog, textures,
|
||||||
buffers (copied when created or updated) and draws, in chunks through the channel; the adapter waits for each to
|
buffers (copied when created or updated) and draws, in chunks through the channel; the adapter waits for each to
|
||||||
be taken, and gives up on a renderer process that stops taking them (the client's renderer carries on).
|
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
|
- 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.
|
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
|
- Speed: on a PC with a graphics card, drawing the whole view (3440x1059, a room of some 220 draws) takes 1.3 to
|
||||||
client finishing a frame to ours being presented averages about 60 ms. The renderer process draws only the
|
3.8 ms a frame, and the time from the client finishing a frame to ours being presented is the same (frames don't
|
||||||
newest frame when it falls behind, and keeps at most one frame queued. Over Remote Desktop, the half drawn by the
|
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
|
||||||
renderer process (another process's window, composited by Windows) visibly trails the client's half by more
|
the right half visibly trails, mostly from compositing another process's window over Remote Desktop. The
|
||||||
than that; that delay comes after presenting, and is to be checked on a real graphics card and display.
|
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
|
||||||
|
|
||||||
|
|||||||
+11
-4
@@ -71,11 +71,18 @@ virtual methods, IMVU's code only uses an engine through helpers that call its v
|
|||||||
vertical field of view, Direct3D's 0 to 1 depth) and an orthographic one for 2D, orthonormal view rotations,
|
vertical field of view, Direct3D's 0 to 1 depth) and an orthographic one for 2D, orthonormal view rotations,
|
||||||
opaque clear colors, ambient light with zero alpha, linear fog (1 to 50), scrolling texture transforms. Furniture
|
opaque clear colors, ambient light with zero alpha, linear fog (1 to 50), scrolling texture transforms. Furniture
|
||||||
vertices are interleaved: positions and normals are 12-byte elements 32 bytes apart, so strides matter.
|
vertices are interleaved: positions and normals are 12-byte elements 32 bytes apart, so strides matter.
|
||||||
|
- Projections give depth from -1 to 1, as OpenGL's do: IMVU's Direct3D renderer multiplies them by a matrix
|
||||||
|
mapping it to 0 to 1 (z' = z / 2 + w / 2) and shifting them half a pixel (-1 / width, +1 / height, for Direct3D
|
||||||
|
9's pixel centers). Without the depth mapping, everything in the nearer half of the depth range is clipped: the
|
||||||
|
2D overlay (name tags, the frame rate, the room's title) is drawn at depth -0.98.
|
||||||
|
- The 2D overlay is ordinary `drawTriangles` with an orthographic projection (a few per frame), textured with text
|
||||||
|
IMVU draws with GDI into bitmaps.
|
||||||
- Particles (`drawPointSprites`): a Visual C++ 2010 `std::vector` of 16-byte points (position, color), drawn as
|
- Particles (`drawPointSprites`): a Visual C++ 2010 `std::vector` of 16-byte points (position, color), drawn as
|
||||||
Direct3D 9 point sprites: squares viewport height / distance pixels across (Direct3D 9's defaults clamp them to
|
Direct3D 9 point sprites: squares viewport height / distance pixels across (IMVU sets the size, 1, and the scale
|
||||||
1 to 64; IMVU sets only the size, 1, and the scale factors 0, 0, 1), textured over the whole square.
|
factors 0, 0, 1), textured over the whole square. Direct3D 9's documented 64-pixel default limit doesn't apply in
|
||||||
- Fog: linear table fog. Direct3D 9 fogs by eye distance only for projections whose (3, 4) element is +1; IMVU's are
|
practice: clamped to it they look much smaller and fainter than the client's.
|
||||||
right-handed (-1), so it fogs by depth buffer value (0 to 1) against start 1 and end 50: in effect no fog.
|
- Fog: linear table fog, start 1 and end 50 in rooms. Fogging by view depth matches the client; by depth buffer
|
||||||
|
value (as Direct3D 9's documentation suggests for projections like IMVU's) doesn't.
|
||||||
- Still unknown: render targets, `CompoundRegion` (texture update
|
- Still unknown: render targets, `CompoundRegion` (texture update
|
||||||
regions), `newRenderTexture`'s flags, and what `beginRendering` does with a null target. MSVC orders overloaded
|
regions), `newRenderTexture`'s flags, and what `beginRendering` does with a null target. MSVC orders overloaded
|
||||||
virtual methods unlike their declaration, so our declarations never overload virtual methods.
|
virtual methods unlike their declaration, so our declarations never overload virtual methods.
|
||||||
|
|||||||
@@ -7,13 +7,15 @@
|
|||||||
|
|
||||||
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
|
||||||
Full,
|
Full,
|
||||||
|
Compare, // over the right half, the left half's scene: both renderers side by side, showing the same
|
||||||
};
|
};
|
||||||
|
|
||||||
// A shared memory section the adapter creates for each engine, named by the renderer process's `--channel <name>`
|
// A shared memory section the adapter creates for each engine, named by the renderer process's `--channel <name>`
|
||||||
@@ -29,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;
|
||||||
|
|||||||
+99
-13
@@ -84,15 +84,90 @@ HWND create_child_window(HWND parent)
|
|||||||
0, 0, 1, 1, parent, nullptr, windowClass.hInstance, nullptr);
|
0, 0, 1, 1, parent, nullptr, windowClass.hInstance, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Where the child window goes in the client's 3D view.
|
// The view, as the adapter set it, or as %TEMP%\t5de-renderer-view.txt says while the client runs (off, split, full
|
||||||
RECT placement(HWND parent, View view)
|
// or compare): for comparing the renderers without restarting the client. Read every second or so.
|
||||||
|
View current_view(View adapter)
|
||||||
{
|
{
|
||||||
RECT client{};
|
static View view = adapter;
|
||||||
GetClientRect(parent, &client);
|
static ULONGLONG checked = 0;
|
||||||
if (view == View::Split) {
|
if (GetTickCount64() - checked < 1000) {
|
||||||
client.left = client.right / 2;
|
return view;
|
||||||
}
|
}
|
||||||
return client;
|
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
|
} // namespace
|
||||||
@@ -203,19 +278,29 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Follow the client's 3D view's size.
|
// Follow the client's 3D view's size.
|
||||||
const RECT place = placement(clientWindow, channel->view);
|
const auto [place, offset] = placement(clientWindow, current_view(channel->view));
|
||||||
const int width = place.right - place.left;
|
const int width = place.right - place.left;
|
||||||
const int height = place.bottom - place.top;
|
const int height = place.bottom - place.top;
|
||||||
if (std::memcmp(&place, &placed, sizeof(RECT)) != 0 && width > 0 && height > 0) {
|
if (std::memcmp(&place, &placed, sizeof(RECT)) != 0) {
|
||||||
SetWindowPos(window, nullptr, place.left, place.top, width, height, SWP_NOZORDER | SWP_NOACTIVATE);
|
|
||||||
swapChain->Resize(static_cast<Uint32>(width), static_cast<Uint32>(height));
|
|
||||||
placed = place;
|
placed = place;
|
||||||
info("drawing {}x{} at {} in the client's 3D view", width, height, place.left);
|
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;
|
LARGE_INTEGER start, end;
|
||||||
QueryPerformanceCounter(&start);
|
QueryPerformanceCounter(&start);
|
||||||
if (scene.execute(std::span(frame).first(complete), place.left, width > 0 && height > 0)) {
|
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);
|
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())) /
|
||||||
@@ -233,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;
|
||||||
|
|||||||
+16
-15
@@ -4,7 +4,6 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "fixed_function_hlsl.hpp"
|
#include "fixed_function_hlsl.hpp"
|
||||||
#include "log.hpp"
|
|
||||||
|
|
||||||
namespace t5de {
|
namespace t5de {
|
||||||
|
|
||||||
@@ -56,7 +55,7 @@ constexpr std::uint32_t hasColors = 4;
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Scene::Scene(IRenderDevice* device, IDeviceContext* context, ISwapChain* swapChain)
|
Scene::Scene(IRenderDevice* device, IDeviceContext* context, ISwapChain* swapChain)
|
||||||
: device_(device), context_(context), swapChain_(swapChain)
|
: device_(device), context_(context), swapChain_(swapChain), shaderSource_(fixedFunctionHlsl)
|
||||||
{
|
{
|
||||||
BufferDesc constants;
|
BufferDesc constants;
|
||||||
constants.Name = "Constants";
|
constants.Name = "Constants";
|
||||||
@@ -86,6 +85,13 @@ Scene::Scene(IRenderDevice* device, IDeviceContext* context, ISwapChain* swapCha
|
|||||||
constants_.flags[2] = 127; // the client's alpha reference
|
constants_.flags[2] = 127; // the client's alpha reference
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Scene::setShaderSource(std::string source)
|
||||||
|
{
|
||||||
|
shaderSource_ = std::move(source);
|
||||||
|
shaders_.clear();
|
||||||
|
pipelines_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
bool Scene::execute(std::span<const std::byte> commands, int offsetX, bool draw)
|
bool Scene::execute(std::span<const std::byte> commands, int offsetX, bool draw)
|
||||||
{
|
{
|
||||||
offsetX_ = offsetX;
|
offsetX_ = offsetX;
|
||||||
@@ -217,6 +223,13 @@ bool Scene::execute(std::span<const std::byte> commands, int offsetX, bool draw)
|
|||||||
void Scene::setProjection(const protocol::SetProjection& projection)
|
void Scene::setProjection(const protocol::SetProjection& projection)
|
||||||
{
|
{
|
||||||
std::memcpy(constants_.projection, projection.m, sizeof(constants_.projection));
|
std::memcpy(constants_.projection, projection.m, sizeof(constants_.projection));
|
||||||
|
// The client's projections give depth from -1 to 1, as OpenGL does; IMVU's Direct3D renderer maps it to 0 to 1
|
||||||
|
// (z' = z / 2 + w / 2) before setting them, and so do we. (It also shifts them half a pixel, which Direct3D 9
|
||||||
|
// needs and Direct3D 11 doesn't.)
|
||||||
|
for (int row = 0; row < 4; ++row) {
|
||||||
|
float* m = constants_.projection + row * 4;
|
||||||
|
m[2] = 0.5f * m[2] + 0.5f * m[3];
|
||||||
|
}
|
||||||
constants_.viewport[0] = static_cast<float>(projection.width);
|
constants_.viewport[0] = static_cast<float>(projection.width);
|
||||||
constants_.viewport[1] = static_cast<float>(projection.height);
|
constants_.viewport[1] = static_cast<float>(projection.height);
|
||||||
if (!onscreen_) {
|
if (!onscreen_) {
|
||||||
@@ -535,18 +548,6 @@ void Scene::drawPointSprites(const protocol::Command& command)
|
|||||||
if (!buffer || prepare(0, true) == nullptr) {
|
if (!buffer || prepare(0, true) == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (spritesLogged_ < 5) {
|
|
||||||
++spritesLogged_;
|
|
||||||
const Texture* texture = textures_.contains(state_.texture0) ? &textures_[state_.texture0] : nullptr;
|
|
||||||
log::info("particles: {}, texture {} {}x{} flags {}, blending {} mode {}, depth test {} write {}, alpha test {} "
|
|
||||||
"reference {}, lighting {}, fog {}, ambient ({:.2f} {:.2f} {:.2f}), texture transform [{} {} {} {} {} {}]",
|
|
||||||
sprites->count, state_.texture0, texture ? texture->width : 0, texture ? texture->height : 0,
|
|
||||||
texture ? texture->flags : 0, state_.blending, state_.blendMode, state_.depthTest, state_.depthWrite,
|
|
||||||
constants_.flags[1], constants_.flags[2], constants_.flags[0], constants_.fog[2], constants_.ambient[0],
|
|
||||||
constants_.ambient[1], constants_.ambient[2], constants_.texTransform0[0], constants_.texTransform0[1],
|
|
||||||
constants_.texTransform0[2], constants_.texTransform0[4], constants_.texTransform0[5],
|
|
||||||
constants_.texTransform0[6]);
|
|
||||||
}
|
|
||||||
IBuffer* buffers[] = {buffer};
|
IBuffer* buffers[] = {buffer};
|
||||||
context_->SetVertexBuffers(0, 1, buffers, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION,
|
context_->SetVertexBuffers(0, 1, buffers, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION,
|
||||||
SET_VERTEX_BUFFERS_FLAG_RESET);
|
SET_VERTEX_BUFFERS_FLAG_RESET);
|
||||||
@@ -565,7 +566,7 @@ const Scene::Shaders& Scene::shaders(std::uint32_t streams, std::uint32_t textur
|
|||||||
const std::string source = std::string("#define HAS_NORMALS ") + ((streams & hasNormals) != 0 ? "1" : "0") +
|
const std::string source = std::string("#define HAS_NORMALS ") + ((streams & hasNormals) != 0 ? "1" : "0") +
|
||||||
"\n#define HAS_TEX_COORDS " + ((streams & hasTexCoords) != 0 ? "1" : "0") +
|
"\n#define HAS_TEX_COORDS " + ((streams & hasTexCoords) != 0 ? "1" : "0") +
|
||||||
"\n#define HAS_COLORS " + ((streams & hasColors) != 0 ? "1" : "0") +
|
"\n#define HAS_COLORS " + ((streams & hasColors) != 0 ? "1" : "0") +
|
||||||
"\n#define TEXTURES " + std::to_string(textures) + "\n" + fixedFunctionHlsl;
|
"\n#define TEXTURES " + std::to_string(textures) + "\n" + shaderSource_;
|
||||||
Shaders created;
|
Shaders created;
|
||||||
ShaderCreateInfo info;
|
ShaderCreateInfo info;
|
||||||
info.SourceLanguage = SHADER_SOURCE_LANGUAGE_HLSL;
|
info.SourceLanguage = SHADER_SOURCE_LANGUAGE_HLSL;
|
||||||
|
|||||||
+5
-1
@@ -6,6 +6,7 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <span>
|
#include <span>
|
||||||
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "EngineFactoryD3D11.h"
|
#include "EngineFactoryD3D11.h"
|
||||||
@@ -23,6 +24,9 @@ public:
|
|||||||
// present).
|
// present).
|
||||||
bool execute(std::span<const std::byte> commands, int offsetX, bool draw);
|
bool execute(std::span<const std::byte> commands, int offsetX, bool draw);
|
||||||
|
|
||||||
|
// Replaces the shaders' source (the built-in one by default): for trying changes while the client runs.
|
||||||
|
void setShaderSource(std::string source);
|
||||||
|
|
||||||
// When the adapter sent the last frame carried out (QueryPerformanceCounter).
|
// When the adapter sent the last frame carried out (QueryPerformanceCounter).
|
||||||
[[nodiscard]] std::uint64_t lastSentAt() const { return lastSentAt_; }
|
[[nodiscard]] std::uint64_t lastSentAt() const { return lastSentAt_; }
|
||||||
|
|
||||||
@@ -111,13 +115,13 @@ private:
|
|||||||
std::unordered_map<std::uint32_t, Buffer> buffers_;
|
std::unordered_map<std::uint32_t, Buffer> buffers_;
|
||||||
std::unordered_map<std::uint32_t, Shaders> shaders_;
|
std::unordered_map<std::uint32_t, Shaders> shaders_;
|
||||||
std::unordered_map<std::uint32_t, Pipeline> pipelines_;
|
std::unordered_map<std::uint32_t, Pipeline> pipelines_;
|
||||||
|
std::string shaderSource_;
|
||||||
|
|
||||||
Constants constants_{};
|
Constants constants_{};
|
||||||
State state_;
|
State state_;
|
||||||
bool onscreen_ = false; // in a frame drawn into the window
|
bool onscreen_ = false; // in a frame drawn into the window
|
||||||
bool draw_ = true;
|
bool draw_ = true;
|
||||||
std::uint64_t lastSentAt_ = 0;
|
std::uint64_t lastSentAt_ = 0;
|
||||||
int spritesLogged_ = 0;
|
|
||||||
int offsetX_ = 0;
|
int offsetX_ = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,8 @@
|
|||||||
// environment's ambient light, plus each light's color times max(0, N.L); no specular or emissive light;
|
// environment's ambient light, plus each light's color times max(0, N.L); no specular or emissive light;
|
||||||
// - textures: none (the lit color), one (times the lit color), or two (the second blended over the first by its
|
// - textures: none (the lit color), one (times the lit color), or two (the second blended over the first by its
|
||||||
// alpha, then times the lit color), both on the vertex's texture coordinates with their own transforms;
|
// alpha, then times the lit color), both on the vertex's texture coordinates with their own transforms;
|
||||||
// - an alpha test passing pixels whose alpha is greater than the reference, then linear fog. Direct3D 9 fogs by
|
// - an alpha test passing pixels whose alpha is greater than the reference, then linear fog by view depth (checked
|
||||||
// distance from the eye only for projections with +1 as their (3, 4) element; IMVU's (right-handed) have -1, so
|
// against the client: fogging by depth buffer value instead looks nothing like it).
|
||||||
// it fogs by the depth buffer's value (0 to 1) against the fog's start and end (in scene units, 1 to 50 in
|
|
||||||
// rooms): in effect, no fog. Reproduced as is.
|
|
||||||
//
|
//
|
||||||
// Particles (vs_sprite) are Direct3D 9 point sprites there: camera-facing squares, viewport height / distance pixels
|
// Particles (vs_sprite) are Direct3D 9 point sprites there: camera-facing squares, viewport height / distance pixels
|
||||||
// across (1 to 64), textured over the whole square; here each is drawn as an instance of a 4-vertex strip.
|
// across (1 to 64), textured over the whole square; here each is drawn as an instance of a 4-vertex strip.
|
||||||
@@ -50,6 +48,7 @@ struct PSInput
|
|||||||
float4 Color : COLOR0;
|
float4 Color : COLOR0;
|
||||||
float2 TexCoord0 : TEXCOORD0;
|
float2 TexCoord0 : TEXCOORD0;
|
||||||
float2 TexCoord1 : TEXCOORD1;
|
float2 TexCoord1 : TEXCOORD1;
|
||||||
|
float Depth : TEXCOORD2;
|
||||||
};
|
};
|
||||||
|
|
||||||
void vs_main(in VSInput input, out PSInput output)
|
void vs_main(in VSInput input, out PSInput output)
|
||||||
@@ -57,6 +56,7 @@ void vs_main(in VSInput input, out PSInput output)
|
|||||||
float4 world = mul(float4(input.Position, 1.0), g_World);
|
float4 world = mul(float4(input.Position, 1.0), g_World);
|
||||||
float4 view = mul(world, g_View);
|
float4 view = mul(world, g_View);
|
||||||
output.Position = mul(view, g_Projection);
|
output.Position = mul(view, g_Projection);
|
||||||
|
output.Depth = abs(view.z);
|
||||||
|
|
||||||
float4 material = float4(1.0, 1.0, 1.0, 1.0);
|
float4 material = float4(1.0, 1.0, 1.0, 1.0);
|
||||||
#if HAS_COLORS
|
#if HAS_COLORS
|
||||||
@@ -101,10 +101,11 @@ void vs_sprite(in SpriteInput input, out PSInput output)
|
|||||||
{
|
{
|
||||||
float4 view = mul(mul(float4(input.Position, 1.0), g_World), g_View);
|
float4 view = mul(mul(float4(input.Position, 1.0), g_World), g_View);
|
||||||
float4 clip = mul(view, g_Projection);
|
float4 clip = mul(view, g_Projection);
|
||||||
float size = clamp(g_Viewport.y / max(length(view.xyz), 1e-4), 1.0, 64.0);
|
float size = clamp(g_Viewport.y / max(length(view.xyz), 1e-4), 1.0, 2048.0);
|
||||||
float2 corner = float2((input.Corner & 1) != 0 ? 1.0 : -1.0, (input.Corner & 2) != 0 ? -1.0 : 1.0);
|
float2 corner = float2((input.Corner & 1) != 0 ? 1.0 : -1.0, (input.Corner & 2) != 0 ? -1.0 : 1.0);
|
||||||
clip.xy += corner * size / g_Viewport.xy * clip.w; // half the size each way, in clip space
|
clip.xy += corner * size / g_Viewport.xy * clip.w; // half the size each way, in clip space
|
||||||
output.Position = clip;
|
output.Position = clip;
|
||||||
|
output.Depth = abs(view.z);
|
||||||
|
|
||||||
// No normals: lit by the ambient light alone.
|
// No normals: lit by the ambient light alone.
|
||||||
float4 material = input.Color.zyxw;
|
float4 material = input.Color.zyxw;
|
||||||
@@ -138,7 +139,7 @@ float4 ps_main(in PSInput input) : SV_Target
|
|||||||
}
|
}
|
||||||
if (g_Fog.z > 0.5)
|
if (g_Fog.z > 0.5)
|
||||||
{
|
{
|
||||||
float visible = saturate((g_Fog.y - input.Position.z) / max(g_Fog.y - g_Fog.x, 1e-6));
|
float visible = saturate((g_Fog.y - input.Depth) / max(g_Fog.y - g_Fog.x, 1e-6));
|
||||||
color.rgb = lerp(g_FogColor.rgb, color.rgb, visible);
|
color.rgb = lerp(g_FogColor.rgb, color.rgb, visible);
|
||||||
}
|
}
|
||||||
return color;
|
return color;
|
||||||
|
|||||||
Reference in new issue
Block a user