Compare commits
12
Commits
5c35c8b63a
..
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a021e2caa9 | ||
|
|
7bf29f3e4c | ||
|
|
97b1d36466 | ||
|
|
2d9da249fa | ||
|
|
64e152de3c | ||
|
|
c48c666309 | ||
|
|
7d17cb0b1f | ||
|
|
3cdda4c7ed | ||
|
|
299cd8c312 | ||
|
|
1a45bf9a86 | ||
|
|
a04dba18c1 | ||
|
|
42fc55cb91 |
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
|
||||
@@ -1,11 +1,14 @@
|
||||
# t5deRenderEngine.dll, the adapter: a renderer plugin for IMVU Classic (32-bit).
|
||||
|
||||
add_library(t5de_render_engine SHARED
|
||||
engine.cpp
|
||||
log.cpp
|
||||
passthrough_engine.cpp
|
||||
pixmap.cpp
|
||||
plugin.cpp
|
||||
plugin.def
|
||||
renderer_process.cpp)
|
||||
recorder.cpp
|
||||
renderer_process.cpp
|
||||
resources.cpp)
|
||||
set_target_properties(t5de_render_engine PROPERTIES OUTPUT_NAME t5deRenderEngine)
|
||||
target_link_libraries(t5de_render_engine PRIVATE t5de::imvu t5de::protocol t5de::options)
|
||||
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
#include "engine.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "log.hpp"
|
||||
#include "resources.hpp"
|
||||
|
||||
namespace t5de {
|
||||
|
||||
using protocol::Op;
|
||||
using protocol::State;
|
||||
|
||||
namespace {
|
||||
|
||||
protocol::SetTransform transform_command(const imvu::Transform& t)
|
||||
{
|
||||
protocol::SetTransform command{};
|
||||
std::memcpy(command.m, &t.linear[0][0], sizeof(float) * 9);
|
||||
command.m[9] = t.translation.x;
|
||||
command.m[10] = t.translation.y;
|
||||
command.m[11] = t.translation.z;
|
||||
return command;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Engine::Engine(imvu::SharedPtr<imvu::PiRenderEngine> inner, std::shared_ptr<Recorder> recorder)
|
||||
: inner_(std::move(inner)), recorder_(std::move(recorder))
|
||||
{
|
||||
}
|
||||
|
||||
Engine::~Engine() { log::info("engine: destroyed after {} frames", frames_); }
|
||||
|
||||
void Engine::setState(State state, std::uint32_t value)
|
||||
{
|
||||
recorder_->add(Op::SetState, protocol::SetState{state, value});
|
||||
}
|
||||
|
||||
void Engine::resizeFrameBuffer(imvu::Vec2i size) { inner_->resizeFrameBuffer(size); }
|
||||
|
||||
void Engine::enable2dFrameBuffer() { inner_->enable2dFrameBuffer(); }
|
||||
|
||||
void Engine::clear(std::uint32_t flags, imvu::Color color)
|
||||
{
|
||||
if (innerDraws()) {
|
||||
inner_->clear(flags, color);
|
||||
}
|
||||
recorder_->add(Op::Clear, protocol::Clear{flags, color});
|
||||
}
|
||||
|
||||
void Engine::setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection)
|
||||
{
|
||||
inner_->setProjection(viewport, projection);
|
||||
if (projection.m[2][3] == 0.0f) {
|
||||
++stats_.flat;
|
||||
}
|
||||
protocol::SetProjection command{viewport.x, viewport.y, {}};
|
||||
std::memcpy(command.m, &projection.m[0][0], sizeof(command.m));
|
||||
recorder_->add(Op::SetProjection, command);
|
||||
}
|
||||
|
||||
void Engine::setModelSpace(const imvu::Transform& model)
|
||||
{
|
||||
inner_->setModelSpace(model);
|
||||
recorder_->add(Op::SetModel, transform_command(model));
|
||||
}
|
||||
|
||||
void Engine::setViewSpace(const imvu::Transform& view)
|
||||
{
|
||||
inner_->setViewSpace(view);
|
||||
recorder_->add(Op::SetView, transform_command(view));
|
||||
}
|
||||
|
||||
void Engine::setTextures(imvu::PiRenderTexture* texture0, imvu::PiRenderTexture* 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)});
|
||||
}
|
||||
|
||||
void Engine::setDepthTestingOn(bool on)
|
||||
{
|
||||
inner_->setDepthTestingOn(on);
|
||||
setState(State::DepthTest, on);
|
||||
}
|
||||
|
||||
void Engine::setDepthWritingOn(bool on)
|
||||
{
|
||||
inner_->setDepthWritingOn(on);
|
||||
setState(State::DepthWrite, on);
|
||||
}
|
||||
|
||||
void Engine::setBlendMode(imvu::RenderBlendMode mode)
|
||||
{
|
||||
inner_->setBlendMode(mode);
|
||||
setState(State::BlendMode, static_cast<std::uint32_t>(mode));
|
||||
}
|
||||
|
||||
void Engine::setAlphaTestingOn(bool on)
|
||||
{
|
||||
inner_->setAlphaTestingOn(on);
|
||||
setState(State::AlphaTest, on);
|
||||
}
|
||||
|
||||
void Engine::setAlphaTestValue(float value)
|
||||
{
|
||||
inner_->setAlphaTestValue(value);
|
||||
setState(State::AlphaReference, static_cast<std::uint32_t>(std::lround(value * 255.0f)));
|
||||
}
|
||||
|
||||
void Engine::setBlendingOn(bool on)
|
||||
{
|
||||
inner_->setBlendingOn(on);
|
||||
setState(State::Blending, on);
|
||||
}
|
||||
|
||||
void Engine::setBackFaceCullingOn(bool on)
|
||||
{
|
||||
inner_->setBackFaceCullingOn(on);
|
||||
setState(State::Culling, on);
|
||||
}
|
||||
|
||||
void Engine::setNormalInvertingOn(bool on)
|
||||
{
|
||||
inner_->setNormalInvertingOn(on);
|
||||
setState(State::NormalInverting, on);
|
||||
}
|
||||
|
||||
void Engine::setLightingOn(bool on)
|
||||
{
|
||||
inner_->setLightingOn(on);
|
||||
setState(State::Lighting, on);
|
||||
}
|
||||
|
||||
void Engine::setLightProperties(std::uint32_t index, const imvu::LightProperties& light)
|
||||
{
|
||||
inner_->setLightProperties(index, light);
|
||||
protocol::SetLight command{index, light.enabled, light.diffuse,
|
||||
light.type == imvu::LightType::Directional,
|
||||
{light.vector.x, light.vector.y, light.vector.z}};
|
||||
// IMVU's Direct3D renderer shines a directional light along -vector, along -x for a zero vector.
|
||||
if (command.directional && light.vector.x == 0 && light.vector.y == 0 && light.vector.z == 0) {
|
||||
command.vector[0] = 1;
|
||||
}
|
||||
recorder_->add(Op::SetLight, command);
|
||||
}
|
||||
|
||||
void Engine::setFogOn(bool on)
|
||||
{
|
||||
inner_->setFogOn(on);
|
||||
setState(State::Fog, on);
|
||||
}
|
||||
|
||||
void Engine::setFogAttributes(float start, float end, imvu::Color color)
|
||||
{
|
||||
inner_->setFogAttributes(start, end, color);
|
||||
recorder_->add(Op::SetFog, protocol::SetFog{start, end, color});
|
||||
}
|
||||
|
||||
void Engine::setTextureTransformMatrixIdentity(std::uint32_t stage)
|
||||
{
|
||||
inner_->setTextureTransformMatrixIdentity(stage);
|
||||
recorder_->add(Op::SetTextureTransform, protocol::SetTextureTransform{stage, 1, {1, 0, 0, 1, 0, 0}});
|
||||
}
|
||||
|
||||
void Engine::setTextureTransformMatrix(std::uint32_t stage, const imvu::TextureTransform& transform)
|
||||
{
|
||||
inner_->setTextureTransformMatrix(stage, transform);
|
||||
protocol::SetTextureTransform command{stage, 0, {}};
|
||||
std::memcpy(command.m, transform.m, sizeof(command.m));
|
||||
recorder_->add(Op::SetTextureTransform, command);
|
||||
}
|
||||
|
||||
void Engine::setEnvironmentAmbient(imvu::Color color)
|
||||
{
|
||||
inner_->setEnvironmentAmbient(color);
|
||||
setState(State::Ambient, color);
|
||||
}
|
||||
|
||||
imvu::PiRenderTexture* Engine::newRenderTexture(const imvu::Pixmap& pixmap, std::uint32_t flags)
|
||||
{
|
||||
imvu::PiRenderTexture* texture = inner_->newRenderTexture(pixmap, flags);
|
||||
if (texture == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return new Texture(texture, recorder_, recorder_->createTexture(pixmap, flags));
|
||||
}
|
||||
|
||||
imvu::PiInternalIndexBuffer* Engine::newIndexBuffer(std::uint32_t count, const std::uint16_t* indices)
|
||||
{
|
||||
imvu::PiInternalIndexBuffer* buffer = inner_->newIndexBuffer(count, indices);
|
||||
if (buffer == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return new IndexBuffer(buffer, recorder_,
|
||||
recorder_->createBuffer(protocol::BufferKind::Index, indices, 2, 2, count));
|
||||
}
|
||||
|
||||
imvu::PiInternalVertexBuffer* Engine::newStaticVertexBuffer(const void* data, std::uint32_t stride,
|
||||
std::uint32_t elementSize, std::uint32_t elementCount)
|
||||
{
|
||||
imvu::PiInternalVertexBuffer* buffer = inner_->newStaticVertexBuffer(data, stride, elementSize, elementCount);
|
||||
if (buffer == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return new VertexBuffer(
|
||||
buffer, recorder_,
|
||||
recorder_->createBuffer(protocol::BufferKind::Vertex, data, stride, elementSize, elementCount));
|
||||
}
|
||||
|
||||
imvu::PiInternalVertexBuffer* Engine::newDynamicVertexBuffer(std::uint32_t elementSize, std::uint32_t elementCount)
|
||||
{
|
||||
imvu::PiInternalVertexBuffer* buffer = inner_->newDynamicVertexBuffer(elementSize, elementCount);
|
||||
if (buffer == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return new VertexBuffer(
|
||||
buffer, recorder_,
|
||||
recorder_->createBuffer(protocol::BufferKind::Vertex, nullptr, 0, elementSize, elementCount));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
recorder_->add(Op::BeginFrame, protocol::BeginFrame{target != nullptr});
|
||||
}
|
||||
|
||||
void Engine::endRendering()
|
||||
{
|
||||
inner_->endRendering();
|
||||
LARGE_INTEGER now;
|
||||
QueryPerformanceCounter(&now);
|
||||
recorder_->add(Op::EndFrame, protocol::EndFrame{static_cast<std::uint64_t>(now.QuadPart)});
|
||||
recorder_->flush(true);
|
||||
++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,
|
||||
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
|
||||
imvu::PiInternalVertexBuffer* colors)
|
||||
{
|
||||
if (innerDraws()) {
|
||||
inner_->drawTriangles(inner(indices), inner(positions), inner(normals), inner(texCoords), inner(colors));
|
||||
}
|
||||
++(id(indices) != 0 && id(positions) != 0 ? stats_.triangles : stats_.foreignTriangles);
|
||||
recorder_->add(Op::DrawTriangles,
|
||||
protocol::DrawTriangles{id(indices), id(positions), id(normals), id(texCoords), id(colors)});
|
||||
}
|
||||
|
||||
void Engine::drawLine(imvu::Vec3f from, imvu::Vec3f to, imvu::Color color)
|
||||
{
|
||||
if (innerDraws()) {
|
||||
inner_->drawLine(from, to, color);
|
||||
}
|
||||
++stats_.lines;
|
||||
}
|
||||
|
||||
void Engine::drawPointSprites(const imvu::PointSpriteVector& sprites)
|
||||
{
|
||||
if (innerDraws()) {
|
||||
inner_->drawPointSprites(sprites);
|
||||
}
|
||||
++stats_.sprites;
|
||||
if (sprites.first == nullptr || sprites.last <= sprites.first) {
|
||||
return;
|
||||
}
|
||||
const auto count = static_cast<std::uint32_t>(sprites.last - sprites.first);
|
||||
if (std::byte* payload = recorder_->add(Op::DrawPointSprites, protocol::DrawPointSprites{count},
|
||||
std::size_t{count} * sizeof(imvu::PointSprite))) {
|
||||
std::memcpy(payload, sprites.first, std::size_t{count} * sizeof(imvu::PointSprite));
|
||||
}
|
||||
}
|
||||
|
||||
void Engine::drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount,
|
||||
const std::uint16_t* indices,
|
||||
const imvu::VertexAttributeArray<imvu::Vec3f>& positions,
|
||||
const imvu::VertexAttributeArray<imvu::Vec3f>& normals,
|
||||
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
|
||||
const imvu::VertexAttributeArray<imvu::Color>& colors)
|
||||
{
|
||||
if (innerDraws()) {
|
||||
inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, colors);
|
||||
}
|
||||
++(indices != nullptr ? stats_.indexed : stats_.unindexed);
|
||||
if (positions.data == nullptr || vertexCount == 0) {
|
||||
return;
|
||||
}
|
||||
// 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 hasTexCoords = texCoords.data != nullptr && texCoords.count >= vertexCount;
|
||||
const bool hasColors = colors.data != nullptr && colors.count >= vertexCount;
|
||||
const std::size_t indexBytes = protocol::padded(std::size_t{indexCount} * 2);
|
||||
const std::size_t size = indexBytes + std::size_t{vertexCount} * (12 + (hasNormals ? 12 : 0) +
|
||||
(hasTexCoords ? 8 : 0) + (hasColors ? 4 : 0));
|
||||
std::byte* payload = recorder_->add(Op::DrawArrays,
|
||||
protocol::DrawArrays{vertexCount, indexCount, hasNormals, hasTexCoords,
|
||||
hasColors},
|
||||
size);
|
||||
if (payload == nullptr) {
|
||||
return;
|
||||
}
|
||||
std::memcpy(payload, indices, std::size_t{indexCount} * 2);
|
||||
std::byte* next = payload + indexBytes;
|
||||
pack(next, positions.data, positions.stride, 12, vertexCount);
|
||||
next += std::size_t{vertexCount} * 12;
|
||||
if (hasNormals) {
|
||||
pack(next, normals.data, normals.stride, 12, vertexCount);
|
||||
next += std::size_t{vertexCount} * 12;
|
||||
}
|
||||
if (hasTexCoords) {
|
||||
pack(next, texCoords.data, texCoords.stride, 8, vertexCount);
|
||||
next += std::size_t{vertexCount} * 8;
|
||||
}
|
||||
if (hasColors) {
|
||||
pack(next, colors.data, colors.stride, 4, vertexCount);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace t5de
|
||||
@@ -1,21 +1,24 @@
|
||||
#pragma once
|
||||
// A renderer that forwards every call to another one (IMVU's Direct3D engine) and logs what a frame is made of: proof
|
||||
// that our declaration of the interface matches IMVU's, and a way to see how the client draws.
|
||||
// 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 <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "renderer_process.hpp"
|
||||
#include "recorder.hpp"
|
||||
#include "t5de/imvu/render_engine.hpp"
|
||||
|
||||
namespace t5de {
|
||||
|
||||
class PassthroughEngine final : public imvu::PiRenderEngine {
|
||||
class Engine final : public imvu::PiRenderEngine {
|
||||
public:
|
||||
// renderer may be null.
|
||||
PassthroughEngine(imvu::SharedPtr<imvu::PiRenderEngine> inner, std::unique_ptr<RendererProcess> renderer);
|
||||
~PassthroughEngine() override;
|
||||
// recorder may be inactive (no renderer process).
|
||||
Engine(imvu::SharedPtr<imvu::PiRenderEngine> inner, std::shared_ptr<Recorder> recorder);
|
||||
~Engine() override;
|
||||
|
||||
void resizeFrameBuffer(imvu::Vec2i size) override;
|
||||
void enable2dFrameBuffer() override;
|
||||
@@ -53,7 +56,7 @@ public:
|
||||
void drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
|
||||
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
|
||||
imvu::PiInternalVertexBuffer* colors) override;
|
||||
void drawLine(imvu::Vec3f from, imvu::Vec3f to, std::uint32_t color) override;
|
||||
void drawLine(imvu::Vec3f from, imvu::Vec3f to, imvu::Color color) override;
|
||||
void drawPointSprites(const imvu::PointSpriteVector& sprites) override;
|
||||
void drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount, const std::uint16_t* indices,
|
||||
const imvu::VertexAttributeArray<imvu::Vec3f>& positions,
|
||||
@@ -61,16 +64,30 @@ public:
|
||||
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
|
||||
const imvu::VertexAttributeArray<imvu::Color>& colors) override;
|
||||
|
||||
// The interface's methods, by vtable slot (slot 0, the destructor, isn't counted).
|
||||
static constexpr std::size_t slots = 35;
|
||||
|
||||
private:
|
||||
void count(std::size_t slot);
|
||||
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::unique_ptr<RendererProcess> renderer_;
|
||||
std::array<std::uint32_t, slots> calls_{};
|
||||
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).
|
||||
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
|
||||
@@ -1,395 +0,0 @@
|
||||
#include "passthrough_engine.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
|
||||
#include "log.hpp"
|
||||
|
||||
namespace t5de {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::array<const char*, PassthroughEngine::slots> names = {
|
||||
"~PiRenderEngine",
|
||||
"resizeFrameBuffer",
|
||||
"enable2dFrameBuffer",
|
||||
"clear",
|
||||
"setProjection",
|
||||
"setModelSpace",
|
||||
"setViewSpace",
|
||||
"setTextures",
|
||||
"setDepthTestingOn",
|
||||
"setDepthWritingOn",
|
||||
"setBlendMode",
|
||||
"setAlphaTestingOn",
|
||||
"setAlphaTestValue",
|
||||
"setBlendingOn",
|
||||
"setBackFaceCullingOn",
|
||||
"setNormalInvertingOn",
|
||||
"setLightingOn",
|
||||
"setLightProperties",
|
||||
"setFogOn",
|
||||
"setFogAttributes",
|
||||
"setTextureTransformMatrixIdentity",
|
||||
"setTextureTransformMatrix",
|
||||
"setEnvironmentAmbient",
|
||||
"newRenderTexture",
|
||||
"newIndexBuffer",
|
||||
"newStaticVertexBuffer",
|
||||
"newDynamicVertexBuffer",
|
||||
"newPixmapRenderTarget",
|
||||
"newTextureRenderTarget",
|
||||
"beginRendering",
|
||||
"endRendering",
|
||||
"drawTriangles",
|
||||
"drawLine",
|
||||
"drawPointSprites",
|
||||
"drawTrianglesFromMemory",
|
||||
};
|
||||
|
||||
// Frames whose calls are logged: the first few, then one a minute or so at 30 FPS.
|
||||
bool logged(std::uint64_t frame) { return frame < 5 || frame % 1800 == 0; }
|
||||
|
||||
// Whether to log a call's arguments: the first few calls of each method, in the whole process. Enough to check that
|
||||
// our layouts of IMVU's types read sensible values.
|
||||
bool sampled(std::size_t slot)
|
||||
{
|
||||
static std::array<std::atomic<int>, PassthroughEngine::slots> calls{};
|
||||
return calls[slot]++ < 3;
|
||||
}
|
||||
|
||||
std::string text(const imvu::Vec3f& v) { return std::format("({:.3f}, {:.3f}, {:.3f})", v.x, v.y, v.z); }
|
||||
|
||||
std::string text(const imvu::Transform& t)
|
||||
{
|
||||
const auto& m = t.linear;
|
||||
return std::format("[{:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f}] + {}", m[0][0], m[0][1],
|
||||
m[0][2], m[1][0], m[1][1], m[1][2], m[2][0], m[2][1], m[2][2], text(t.translation));
|
||||
}
|
||||
|
||||
// A pixmap's size, from pixmap.dll's exported Pixmap::width and Pixmap::height.
|
||||
imvu::Vec2i pixmap_size(const imvu::Pixmap& pixmap)
|
||||
{
|
||||
using Getter = unsigned(__thiscall*)(const imvu::Pixmap*);
|
||||
struct Getters {
|
||||
Getter width;
|
||||
Getter height;
|
||||
};
|
||||
static const Getters getters = [] {
|
||||
HMODULE module = GetModuleHandleW(L"pixmap.dll");
|
||||
if (module == nullptr) {
|
||||
return Getters{};
|
||||
}
|
||||
return Getters{reinterpret_cast<Getter>(GetProcAddress(module, "?width@Pixmap@@QBEIXZ")),
|
||||
reinterpret_cast<Getter>(GetProcAddress(module, "?height@Pixmap@@QBEIXZ"))};
|
||||
}();
|
||||
if (getters.width == nullptr || getters.height == nullptr) {
|
||||
return {-1, -1};
|
||||
}
|
||||
return {static_cast<int>(getters.width(&pixmap)), static_cast<int>(getters.height(&pixmap))};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PassthroughEngine::PassthroughEngine(imvu::SharedPtr<imvu::PiRenderEngine> inner,
|
||||
std::unique_ptr<RendererProcess> renderer)
|
||||
: inner_(std::move(inner)), renderer_(std::move(renderer))
|
||||
{
|
||||
log::info("passthrough: wrapping the engine at {}", static_cast<void*>(inner_.get()));
|
||||
}
|
||||
|
||||
PassthroughEngine::~PassthroughEngine() { log::info("passthrough: destroyed after {} frames", frames_); }
|
||||
|
||||
void PassthroughEngine::count(std::size_t slot) { ++calls_[slot]; }
|
||||
|
||||
void PassthroughEngine::resizeFrameBuffer(imvu::Vec2i size)
|
||||
{
|
||||
log::info("resizeFrameBuffer({}, {})", size.x, size.y);
|
||||
count(1);
|
||||
inner_->resizeFrameBuffer(size);
|
||||
}
|
||||
|
||||
void PassthroughEngine::enable2dFrameBuffer()
|
||||
{
|
||||
count(2);
|
||||
inner_->enable2dFrameBuffer();
|
||||
}
|
||||
|
||||
void PassthroughEngine::clear(std::uint32_t flags, imvu::Color color)
|
||||
{
|
||||
if (sampled(3)) {
|
||||
log::info("clear(flags {}, color {:08x})", flags, color);
|
||||
}
|
||||
count(3);
|
||||
inner_->clear(flags, color);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection)
|
||||
{
|
||||
if (sampled(4)) {
|
||||
const auto& m = projection.m;
|
||||
log::info("setProjection({}x{}, [{:.3f} {:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f} {:.3f}; "
|
||||
"{:.3f} {:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f} {:.3f}])",
|
||||
viewport.x, viewport.y, m[0][0], m[0][1], m[0][2], m[0][3], m[1][0], m[1][1], m[1][2],
|
||||
m[1][3], m[2][0], m[2][1], m[2][2], m[2][3], m[3][0], m[3][1], m[3][2], m[3][3]);
|
||||
}
|
||||
count(4);
|
||||
inner_->setProjection(viewport, projection);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setModelSpace(const imvu::Transform& model)
|
||||
{
|
||||
if (sampled(5)) {
|
||||
log::info("setModelSpace({})", text(model));
|
||||
}
|
||||
count(5);
|
||||
inner_->setModelSpace(model);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setViewSpace(const imvu::Transform& view)
|
||||
{
|
||||
if (sampled(6)) {
|
||||
log::info("setViewSpace({})", text(view));
|
||||
}
|
||||
count(6);
|
||||
inner_->setViewSpace(view);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setTextures(imvu::PiRenderTexture* texture0, imvu::PiRenderTexture* texture1)
|
||||
{
|
||||
count(7);
|
||||
inner_->setTextures(texture0, texture1);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setDepthTestingOn(bool on)
|
||||
{
|
||||
count(8);
|
||||
inner_->setDepthTestingOn(on);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setDepthWritingOn(bool on)
|
||||
{
|
||||
count(9);
|
||||
inner_->setDepthWritingOn(on);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setBlendMode(imvu::RenderBlendMode mode)
|
||||
{
|
||||
if (sampled(10)) {
|
||||
log::info("setBlendMode({})", static_cast<int>(mode));
|
||||
}
|
||||
count(10);
|
||||
inner_->setBlendMode(mode);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setAlphaTestingOn(bool on)
|
||||
{
|
||||
count(11);
|
||||
inner_->setAlphaTestingOn(on);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setAlphaTestValue(float value)
|
||||
{
|
||||
if (sampled(12)) {
|
||||
log::info("setAlphaTestValue({:.3f})", value);
|
||||
}
|
||||
count(12);
|
||||
inner_->setAlphaTestValue(value);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setBlendingOn(bool on)
|
||||
{
|
||||
count(13);
|
||||
inner_->setBlendingOn(on);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setBackFaceCullingOn(bool on)
|
||||
{
|
||||
count(14);
|
||||
inner_->setBackFaceCullingOn(on);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setNormalInvertingOn(bool on)
|
||||
{
|
||||
count(15);
|
||||
inner_->setNormalInvertingOn(on);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setLightingOn(bool on)
|
||||
{
|
||||
count(16);
|
||||
inner_->setLightingOn(on);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setLightProperties(std::uint32_t index, const imvu::LightProperties& light)
|
||||
{
|
||||
if (sampled(17)) {
|
||||
log::info("setLightProperties({}, enabled {}, diffuse {:08x}, vector {}, type {})", index,
|
||||
light.enabled, light.diffuse, text(light.vector), static_cast<int>(light.type));
|
||||
}
|
||||
count(17);
|
||||
inner_->setLightProperties(index, light);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setFogOn(bool on)
|
||||
{
|
||||
count(18);
|
||||
inner_->setFogOn(on);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setFogAttributes(float start, float end, imvu::Color color)
|
||||
{
|
||||
if (sampled(19)) {
|
||||
log::info("setFogAttributes(start {:.3f}, end {:.3f}, color {:08x})", start, end, color);
|
||||
}
|
||||
count(19);
|
||||
inner_->setFogAttributes(start, end, color);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setTextureTransformMatrixIdentity(std::uint32_t stage)
|
||||
{
|
||||
count(20);
|
||||
inner_->setTextureTransformMatrixIdentity(stage);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setTextureTransformMatrix(std::uint32_t stage, const imvu::TextureTransform& transform)
|
||||
{
|
||||
if (sampled(21)) {
|
||||
const auto& m = transform.m;
|
||||
log::info("setTextureTransformMatrix({}, [{:.3f} {:.3f} {:.3f} {:.3f} {:.3f} {:.3f}])", stage, m[0],
|
||||
m[1], m[2], m[3], m[4], m[5]);
|
||||
}
|
||||
count(21);
|
||||
inner_->setTextureTransformMatrix(stage, transform);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setEnvironmentAmbient(imvu::Color color)
|
||||
{
|
||||
if (sampled(22)) {
|
||||
log::info("setEnvironmentAmbient({:08x})", color);
|
||||
}
|
||||
count(22);
|
||||
inner_->setEnvironmentAmbient(color);
|
||||
}
|
||||
|
||||
imvu::PiRenderTexture* PassthroughEngine::newRenderTexture(const imvu::Pixmap& pixmap, std::uint32_t flags)
|
||||
{
|
||||
if (sampled(23)) {
|
||||
const auto size = pixmap_size(pixmap);
|
||||
log::info("newRenderTexture({}x{} pixmap, flags {:x})", size.x, size.y, flags);
|
||||
}
|
||||
count(23);
|
||||
return inner_->newRenderTexture(pixmap, flags);
|
||||
}
|
||||
|
||||
imvu::PiInternalIndexBuffer* PassthroughEngine::newIndexBuffer(std::uint32_t indexCount, const std::uint16_t* indices)
|
||||
{
|
||||
if (sampled(24)) {
|
||||
log::info("newIndexBuffer({} indices)", indexCount);
|
||||
}
|
||||
count(24);
|
||||
return inner_->newIndexBuffer(indexCount, indices);
|
||||
}
|
||||
|
||||
imvu::PiInternalVertexBuffer* PassthroughEngine::newStaticVertexBuffer(const void* data, std::uint32_t stride,
|
||||
std::uint32_t elementSize, std::uint32_t elementCount)
|
||||
{
|
||||
if (sampled(25)) {
|
||||
log::info("newStaticVertexBuffer(stride {}, element size {}, {} elements)", stride, elementSize,
|
||||
elementCount);
|
||||
}
|
||||
count(25);
|
||||
return inner_->newStaticVertexBuffer(data, stride, elementSize, elementCount);
|
||||
}
|
||||
|
||||
imvu::PiInternalVertexBuffer* PassthroughEngine::newDynamicVertexBuffer(std::uint32_t elementSize, std::uint32_t elementCount)
|
||||
{
|
||||
if (sampled(26)) {
|
||||
log::info("newDynamicVertexBuffer(element size {}, {} elements)", elementSize, elementCount);
|
||||
}
|
||||
count(26);
|
||||
return inner_->newDynamicVertexBuffer(elementSize, elementCount);
|
||||
}
|
||||
|
||||
imvu::SharedPtr<imvu::PiPixmapRenderTarget> PassthroughEngine::newPixmapRenderTarget(imvu::Vec2i size)
|
||||
{
|
||||
log::info("newPixmapRenderTarget({}, {})", size.x, size.y);
|
||||
count(27);
|
||||
return inner_->newPixmapRenderTarget(size);
|
||||
}
|
||||
|
||||
imvu::SharedPtr<imvu::PiTextureRenderTarget> PassthroughEngine::newTextureRenderTarget(imvu::Vec2i size)
|
||||
{
|
||||
log::info("newTextureRenderTarget({}, {})", size.x, size.y);
|
||||
count(28);
|
||||
return inner_->newTextureRenderTarget(size);
|
||||
}
|
||||
|
||||
void PassthroughEngine::beginRendering(imvu::PiRenderTarget* target)
|
||||
{
|
||||
count(29);
|
||||
inner_->beginRendering(target);
|
||||
}
|
||||
|
||||
void PassthroughEngine::endRendering()
|
||||
{
|
||||
count(30);
|
||||
inner_->endRendering();
|
||||
if (renderer_) {
|
||||
renderer_->frameDrawn();
|
||||
}
|
||||
if (logged(frames_)) {
|
||||
std::string line = std::format("frame {}:", frames_);
|
||||
for (std::size_t slot = 1; slot < slots; ++slot) {
|
||||
if (calls_[slot] != 0) {
|
||||
line += std::format(" {} x{},", names[slot], calls_[slot]);
|
||||
}
|
||||
}
|
||||
line.pop_back();
|
||||
log::write(line);
|
||||
}
|
||||
calls_ = {};
|
||||
++frames_;
|
||||
}
|
||||
|
||||
void PassthroughEngine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
|
||||
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
|
||||
imvu::PiInternalVertexBuffer* colors)
|
||||
{
|
||||
count(31);
|
||||
inner_->drawTriangles(indices, positions, normals, texCoords, colors);
|
||||
}
|
||||
|
||||
void PassthroughEngine::drawLine(imvu::Vec3f from, imvu::Vec3f to, std::uint32_t color)
|
||||
{
|
||||
if (sampled(32)) {
|
||||
log::info("drawLine({}, {}, {:08x})", text(from), text(to), color);
|
||||
}
|
||||
count(32);
|
||||
inner_->drawLine(from, to, color);
|
||||
}
|
||||
|
||||
void PassthroughEngine::drawPointSprites(const imvu::PointSpriteVector& sprites)
|
||||
{
|
||||
count(33);
|
||||
inner_->drawPointSprites(sprites);
|
||||
}
|
||||
|
||||
void PassthroughEngine::drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount,
|
||||
const std::uint16_t* indices,
|
||||
const imvu::VertexAttributeArray<imvu::Vec3f>& positions,
|
||||
const imvu::VertexAttributeArray<imvu::Vec3f>& normals,
|
||||
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
|
||||
const imvu::VertexAttributeArray<imvu::Color>& colors)
|
||||
{
|
||||
if (sampled(34)) {
|
||||
log::info("drawTrianglesFromMemory({} vertices, {} indices; positions {} stride {}, colors {} stride {})",
|
||||
vertexCount, indexCount, positions.count, positions.stride, colors.count, colors.stride);
|
||||
}
|
||||
count(34);
|
||||
inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, colors);
|
||||
}
|
||||
|
||||
} // namespace t5de
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "pixmap.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace t5de {
|
||||
|
||||
namespace {
|
||||
|
||||
using Getter = unsigned(__thiscall*)(const imvu::Pixmap*);
|
||||
using PixelsGetter = std::uint32_t*(__thiscall*)(const imvu::Pixmap*);
|
||||
|
||||
struct Methods {
|
||||
Getter width = nullptr;
|
||||
Getter height = nullptr;
|
||||
Getter rowBytes = nullptr;
|
||||
PixelsGetter pixels = nullptr;
|
||||
};
|
||||
|
||||
const Methods& methods()
|
||||
{
|
||||
static const Methods methods = [] {
|
||||
Methods found;
|
||||
if (HMODULE module = GetModuleHandleW(L"pixmap.dll")) {
|
||||
found.width = reinterpret_cast<Getter>(GetProcAddress(module, "?width@Pixmap@@QBEIXZ"));
|
||||
found.height = reinterpret_cast<Getter>(GetProcAddress(module, "?height@Pixmap@@QBEIXZ"));
|
||||
found.rowBytes = reinterpret_cast<Getter>(GetProcAddress(module, "?rowBytes@Pixmap@@QBEIXZ"));
|
||||
found.pixels = reinterpret_cast<PixelsGetter>(GetProcAddress(module, "?pixels@Pixmap@@QBEPAIXZ"));
|
||||
}
|
||||
return found;
|
||||
}();
|
||||
return methods;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<PixmapPixels> pixmap_pixels(const imvu::Pixmap& pixmap)
|
||||
{
|
||||
const Methods& m = methods();
|
||||
if (m.width == nullptr || m.height == nullptr || m.rowBytes == nullptr || m.pixels == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return PixmapPixels{m.width(&pixmap), m.height(&pixmap), m.rowBytes(&pixmap), m.pixels(&pixmap)};
|
||||
}
|
||||
|
||||
} // namespace t5de
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
// Reading IMVU's images (Pixmap) through pixmap.dll's exported methods, rather than depending on their layout.
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
#include "t5de/imvu/types.hpp"
|
||||
|
||||
namespace t5de {
|
||||
|
||||
struct PixmapPixels {
|
||||
std::uint32_t width;
|
||||
std::uint32_t height;
|
||||
std::uint32_t rowBytes;
|
||||
const std::uint32_t* pixels; // 0xAARRGGBB, rows rowBytes apart
|
||||
};
|
||||
|
||||
// Null if pixmap.dll's methods can't be found.
|
||||
std::optional<PixmapPixels> pixmap_pixels(const imvu::Pixmap& pixmap);
|
||||
|
||||
} // namespace t5de
|
||||
+22
-5
@@ -1,12 +1,15 @@
|
||||
// The renderer plugin's entry points, which SceneWindow.dll's createDLLRenderer looks up by name (exported
|
||||
// undecorated by plugin.def). For now the renderer is the pass-through: IMVU's Direct3D engine, wrapped.
|
||||
// undecorated by plugin.def). The engine is the adapter's (engine.hpp): IMVU's Direct3D engine draws, and the
|
||||
// renderer process draws over part of it.
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <exception>
|
||||
#include <string_view>
|
||||
|
||||
#include "log.hpp"
|
||||
#include "passthrough_engine.hpp"
|
||||
#include "engine.hpp"
|
||||
#include "recorder.hpp"
|
||||
#include "renderer_process.hpp"
|
||||
#include "t5de/imvu/render_engine.hpp"
|
||||
|
||||
@@ -30,6 +33,19 @@ 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 right half), full (the default) or compare.
|
||||
// (The renderer process can also be switched while it runs: renderer/main.cpp.)
|
||||
protocol::View view()
|
||||
{
|
||||
wchar_t value[16] = {};
|
||||
GetEnvironmentVariableW(L"T5DE_RENDERER_VIEW", value, 16);
|
||||
const std::wstring_view setting = value;
|
||||
return setting == L"off" ? protocol::View::Off
|
||||
: setting == L"split" ? protocol::View::Split
|
||||
: setting == L"compare" ? protocol::View::Compare
|
||||
: protocol::View::Full;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int __cdecl isRenderEngineSupported() { return 1; }
|
||||
@@ -45,9 +61,10 @@ extern "C" imvu::SharedPtr<imvu::PiRenderEngine> __cdecl createRenderEngine(imvu
|
||||
if (!inner) {
|
||||
return {};
|
||||
}
|
||||
auto renderer = hwnd != nullptr ? RendererProcess::start(hwnd) : nullptr;
|
||||
return imvu::SharedPtr<imvu::PiRenderEngine>(
|
||||
std::make_unique<PassthroughEngine>(std::move(inner), std::move(renderer)));
|
||||
const protocol::View shown = view();
|
||||
auto renderer = hwnd != nullptr && shown != protocol::View::Off ? RendererProcess::start(hwnd, shown) : nullptr;
|
||||
return imvu::SharedPtr<imvu::PiRenderEngine>(std::make_unique<Engine>(
|
||||
std::move(inner), std::make_shared<Recorder>(std::move(renderer))));
|
||||
} catch (const std::exception& e) {
|
||||
// Exceptions mustn't cross into IMVU's code (a different C++ runtime).
|
||||
log::info("createRenderEngine failed: {}", e.what());
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
#include "recorder.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "log.hpp"
|
||||
#include "pixmap.hpp"
|
||||
|
||||
namespace t5de {
|
||||
|
||||
namespace {
|
||||
|
||||
// Between frames, recorded commands (mostly textures and buffers being loaded) are sent once they reach this size.
|
||||
constexpr std::size_t flushSize = 4 * 1024 * 1024;
|
||||
|
||||
} // namespace
|
||||
|
||||
void pack(std::byte* destination, const void* source, std::uint32_t stride, std::uint32_t elementSize,
|
||||
std::uint32_t count)
|
||||
{
|
||||
const auto* from = static_cast<const std::byte*>(source);
|
||||
if (stride == 0 || stride == elementSize) {
|
||||
std::memcpy(destination, from, std::size_t{elementSize} * count);
|
||||
return;
|
||||
}
|
||||
for (std::uint32_t i = 0; i < count; ++i) {
|
||||
std::memcpy(destination + std::size_t{i} * elementSize, from + std::size_t{i} * stride, elementSize);
|
||||
}
|
||||
}
|
||||
|
||||
Recorder::Recorder(std::unique_ptr<RendererProcess> process) : process_(std::move(process)) {}
|
||||
|
||||
void Recorder::flush(bool endsFrame)
|
||||
{
|
||||
if (!active()) {
|
||||
writer_.clear();
|
||||
return;
|
||||
}
|
||||
process_->send(writer_.bytes(), endsFrame);
|
||||
writer_.clear();
|
||||
}
|
||||
|
||||
void Recorder::flushIfLarge()
|
||||
{
|
||||
if (writer_.size() >= flushSize) {
|
||||
flush(false);
|
||||
}
|
||||
}
|
||||
|
||||
void Recorder::addPixels(protocol::Op op, std::uint32_t id, const imvu::Pixmap& pixmap, std::uint32_t flags)
|
||||
{
|
||||
const auto pixels = pixmap_pixels(pixmap);
|
||||
if (!pixels || pixels->pixels == nullptr) {
|
||||
return;
|
||||
}
|
||||
const std::size_t rowSize = std::size_t{pixels->width} * 4;
|
||||
std::byte* payload = nullptr;
|
||||
if (op == protocol::Op::CreateTexture) {
|
||||
payload = add(op, protocol::CreateTexture{id, pixels->width, pixels->height, flags}, rowSize * pixels->height);
|
||||
} else {
|
||||
payload = add(op, protocol::UpdateTexture{id, pixels->width, pixels->height}, rowSize * pixels->height);
|
||||
}
|
||||
if (payload == nullptr) {
|
||||
return;
|
||||
}
|
||||
const auto* rows = reinterpret_cast<const std::byte*>(pixels->pixels);
|
||||
for (std::uint32_t y = 0; y < pixels->height; ++y) {
|
||||
std::memcpy(payload + y * rowSize, rows + std::size_t{y} * pixels->rowBytes, rowSize);
|
||||
}
|
||||
flushIfLarge();
|
||||
}
|
||||
|
||||
std::uint32_t Recorder::createTexture(const imvu::Pixmap& pixmap, std::uint32_t flags)
|
||||
{
|
||||
const std::uint32_t id = nextId_++;
|
||||
addPixels(protocol::Op::CreateTexture, id, pixmap, flags);
|
||||
return id;
|
||||
}
|
||||
|
||||
void Recorder::updateTexture(std::uint32_t id, const imvu::Pixmap& pixmap)
|
||||
{
|
||||
addPixels(protocol::Op::UpdateTexture, id, pixmap, 0);
|
||||
}
|
||||
|
||||
void Recorder::destroyTexture(std::uint32_t id) { add(protocol::Op::DestroyTexture, protocol::DestroyTexture{id}); }
|
||||
|
||||
std::uint32_t Recorder::createBuffer(protocol::BufferKind kind, const void* data, std::uint32_t stride,
|
||||
std::uint32_t elementSize, std::uint32_t count)
|
||||
{
|
||||
const std::uint32_t id = nextId_++;
|
||||
const std::size_t size = data != nullptr ? std::size_t{elementSize} * count : 0;
|
||||
if (std::byte* payload = add(protocol::Op::CreateBuffer, protocol::CreateBuffer{id, kind, elementSize, count}, size);
|
||||
payload != nullptr && size != 0) {
|
||||
pack(payload, data, stride, elementSize, count);
|
||||
}
|
||||
flushIfLarge();
|
||||
return id;
|
||||
}
|
||||
|
||||
void Recorder::updateBuffer(std::uint32_t id, const void* data, std::uint32_t stride, std::uint32_t elementSize,
|
||||
std::uint32_t count)
|
||||
{
|
||||
const std::size_t size = std::size_t{elementSize} * count;
|
||||
if (std::byte* payload = add(protocol::Op::UpdateBuffer, protocol::UpdateBuffer{id, elementSize, count}, size);
|
||||
payload != nullptr && data != nullptr) {
|
||||
pack(payload, data, stride, elementSize, count);
|
||||
}
|
||||
}
|
||||
|
||||
void Recorder::destroyBuffer(std::uint32_t id) { add(protocol::Op::DestroyBuffer, protocol::DestroyBuffer{id}); }
|
||||
|
||||
} // namespace t5de
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
// Records what the client draws as commands (include/t5de/protocol/commands.hpp) and sends them to the renderer
|
||||
// process. Shared by an engine and the resources it creates (which can outlive it); once the renderer process is
|
||||
// gone, it records nothing.
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "renderer_process.hpp"
|
||||
#include "t5de/imvu/types.hpp"
|
||||
#include "t5de/protocol/commands.hpp"
|
||||
|
||||
namespace t5de {
|
||||
|
||||
class Recorder {
|
||||
public:
|
||||
explicit Recorder(std::unique_ptr<RendererProcess> process);
|
||||
|
||||
[[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)
|
||||
{
|
||||
if (!active()) {
|
||||
return nullptr;
|
||||
}
|
||||
std::byte* payload = writer_.add(op, body, payloadSize);
|
||||
return payload;
|
||||
}
|
||||
|
||||
// Sends what's recorded: at the end of each frame, and between frames when a lot is waiting (resources).
|
||||
void flush(bool endsFrame);
|
||||
void flushIfLarge();
|
||||
|
||||
std::uint32_t createTexture(const imvu::Pixmap& pixmap, std::uint32_t flags);
|
||||
void updateTexture(std::uint32_t id, const imvu::Pixmap& pixmap);
|
||||
void destroyTexture(std::uint32_t id);
|
||||
|
||||
// Copies count elements of elementSize bytes, stride bytes apart (0: packed), from data (null: none yet).
|
||||
std::uint32_t createBuffer(protocol::BufferKind kind, const void* data, std::uint32_t stride,
|
||||
std::uint32_t elementSize, std::uint32_t count);
|
||||
void updateBuffer(std::uint32_t id, const void* data, std::uint32_t stride, std::uint32_t elementSize,
|
||||
std::uint32_t count);
|
||||
void destroyBuffer(std::uint32_t id);
|
||||
|
||||
private:
|
||||
void addPixels(protocol::Op op, std::uint32_t id, const imvu::Pixmap& pixmap, std::uint32_t flags);
|
||||
|
||||
std::unique_ptr<RendererProcess> process_;
|
||||
protocol::Writer writer_;
|
||||
std::uint32_t nextId_ = 1;
|
||||
};
|
||||
|
||||
// Copies count elements of elementSize bytes, stride bytes apart (0: packed), into destination, packed.
|
||||
void pack(std::byte* destination, const void* source, std::uint32_t stride, std::uint32_t elementSize,
|
||||
std::uint32_t count);
|
||||
|
||||
} // namespace t5de
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "renderer_process.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <string>
|
||||
@@ -11,6 +13,9 @@ namespace t5de {
|
||||
|
||||
namespace {
|
||||
|
||||
// How long to wait for the renderer process to take a chunk before giving up on it.
|
||||
constexpr DWORD consumeTimeout = 5000;
|
||||
|
||||
// The folder this DLL is in: the client's program folder.
|
||||
std::filesystem::path module_folder()
|
||||
{
|
||||
@@ -22,36 +27,73 @@ std::filesystem::path module_folder()
|
||||
return std::filesystem::path(path).parent_path();
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
std::unique_ptr<RendererProcess> RendererProcess::start(HWND window)
|
||||
std::unique_ptr<RendererProcess> RendererProcess::start(HWND window, protocol::View view)
|
||||
{
|
||||
static std::atomic<int> engines = 0;
|
||||
const std::wstring name = std::format(L"Local\\t5de-renderer-{}-{}", GetCurrentProcessId(), engines++);
|
||||
constexpr DWORD size = sizeof(protocol::Channel) + protocol::channelCapacity;
|
||||
|
||||
std::unique_ptr<RendererProcess> renderer(new RendererProcess());
|
||||
renderer->mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0,
|
||||
sizeof(protocol::Channel), name.c_str());
|
||||
renderer->mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, size, name.c_str());
|
||||
if (renderer->mapping_ == nullptr) {
|
||||
log::info("renderer process: can't create the channel: error {}", GetLastError());
|
||||
return nullptr;
|
||||
}
|
||||
renderer->channel_ = static_cast<protocol::Channel*>(
|
||||
MapViewOfFile(renderer->mapping_, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(protocol::Channel)));
|
||||
renderer->frameEvent_ = CreateEventW(nullptr, FALSE, FALSE, (name + protocol::frameEventSuffix).c_str());
|
||||
if (renderer->channel_ == nullptr || renderer->frameEvent_ == nullptr) {
|
||||
renderer->channel_ =
|
||||
static_cast<protocol::Channel*>(MapViewOfFile(renderer->mapping_, FILE_MAP_ALL_ACCESS, 0, 0, size));
|
||||
renderer->ready_ = CreateEventW(nullptr, FALSE, FALSE, (name + protocol::readyEventSuffix).c_str());
|
||||
renderer->consumed_ = CreateEventW(nullptr, FALSE, TRUE, (name + protocol::consumedEventSuffix).c_str());
|
||||
if (renderer->channel_ == nullptr || renderer->ready_ == nullptr || renderer->consumed_ == nullptr) {
|
||||
log::info("renderer process: can't set up the channel: error {}", GetLastError());
|
||||
return nullptr;
|
||||
}
|
||||
renderer->data_ = reinterpret_cast<std::byte*>(renderer->channel_ + 1);
|
||||
*renderer->channel_ = {.version = protocol::version,
|
||||
.closing = 0,
|
||||
.window = reinterpret_cast<std::uintptr_t>(window),
|
||||
.frames = 0};
|
||||
.frames = 0,
|
||||
.view = view,
|
||||
.capacity = protocol::channelCapacity,
|
||||
.chunkSize = 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.
|
||||
SetWindowLongW(window, GWL_STYLE, GetWindowLongW(window, GWL_STYLE) | WS_CLIPCHILDREN);
|
||||
|
||||
const auto exe = module_folder() / L"t5de-renderer.exe";
|
||||
// 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)};
|
||||
PROCESS_INFORMATION process{};
|
||||
@@ -62,8 +104,7 @@ std::unique_ptr<RendererProcess> RendererProcess::start(HWND window)
|
||||
}
|
||||
CloseHandle(process.hThread);
|
||||
renderer->process_ = process.hProcess;
|
||||
log::info("renderer process: started {} (pid {}) on channel {}", exe.string(), process.dwProcessId,
|
||||
std::filesystem::path(name).string());
|
||||
log::info("renderer process: started {} (pid {}) on channel {}", exe.string(), process.dwProcessId, narrow(name));
|
||||
return renderer;
|
||||
}
|
||||
|
||||
@@ -71,22 +112,42 @@ RendererProcess::~RendererProcess()
|
||||
{
|
||||
if (channel_ != nullptr) {
|
||||
channel_->closing = 1;
|
||||
if (frameEvent_ != nullptr) {
|
||||
SetEvent(frameEvent_);
|
||||
if (ready_ != nullptr) {
|
||||
SetEvent(ready_);
|
||||
}
|
||||
UnmapViewOfFile(channel_);
|
||||
}
|
||||
for (HANDLE handle : {frameEvent_, mapping_, process_}) {
|
||||
for (HANDLE handle : {ready_, consumed_, mapping_, process_}) {
|
||||
if (handle != nullptr) {
|
||||
CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RendererProcess::frameDrawn()
|
||||
bool RendererProcess::send(std::span<const std::byte> commands, bool endsFrame)
|
||||
{
|
||||
if (!alive_) {
|
||||
return false;
|
||||
}
|
||||
do {
|
||||
const HANDLE waits[] = {consumed_, process_};
|
||||
const DWORD result = WaitForMultipleObjects(2, waits, FALSE, consumeTimeout);
|
||||
if (result != WAIT_OBJECT_0) {
|
||||
log::info("renderer process: {}; giving up on it", result == WAIT_OBJECT_0 + 1 ? "exited" : "not responding");
|
||||
alive_ = false;
|
||||
return false;
|
||||
}
|
||||
const std::size_t size = std::min<std::size_t>(commands.size(), channel_->capacity);
|
||||
std::memcpy(data_, commands.data(), size);
|
||||
commands = commands.subspan(size);
|
||||
channel_->chunkSize = static_cast<std::uint32_t>(size);
|
||||
channel_->chunkFlags = endsFrame && commands.empty() ? protocol::chunkEndsFrame : 0;
|
||||
if (endsFrame && commands.empty()) {
|
||||
++channel_->frames;
|
||||
SetEvent(frameEvent_);
|
||||
}
|
||||
SetEvent(ready_);
|
||||
} while (!commands.empty());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace t5de
|
||||
@@ -1,11 +1,12 @@
|
||||
#pragma once
|
||||
// The renderer process (t5de-renderer.exe, 64-bit) an engine draws through: started with the engine, told about
|
||||
// each frame through a shared memory channel (include/t5de/protocol/channel.hpp), and told to exit with it.
|
||||
// The renderer process (t5de-renderer.exe, 64-bit) an engine draws through: started with the engine, sent the
|
||||
// command stream through a shared memory channel (include/t5de/protocol/channel.hpp), and told to exit with it.
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
|
||||
#include "t5de/protocol/channel.hpp"
|
||||
|
||||
@@ -13,23 +14,32 @@ namespace t5de {
|
||||
|
||||
class RendererProcess {
|
||||
public:
|
||||
// Starts t5de-renderer.exe from this DLL's folder, drawing into window; null if it can't.
|
||||
static std::unique_ptr<RendererProcess> start(HWND window);
|
||||
// Starts t5de-renderer.exe from this DLL's folder, drawing into part of window; null if it can't.
|
||||
static std::unique_ptr<RendererProcess> start(HWND window, protocol::View view);
|
||||
|
||||
RendererProcess(const RendererProcess&) = delete;
|
||||
RendererProcess& operator=(const RendererProcess&) = delete;
|
||||
~RendererProcess();
|
||||
|
||||
// The client drew a frame.
|
||||
void frameDrawn();
|
||||
// Sends commands, in as many chunks as they take; endsFrame marks the end of a frame. False if the renderer
|
||||
// process is gone or stopped taking them (it's then given up on).
|
||||
bool send(std::span<const std::byte> commands, bool endsFrame);
|
||||
|
||||
[[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;
|
||||
|
||||
HANDLE mapping_ = nullptr;
|
||||
HANDLE frameEvent_ = nullptr;
|
||||
HANDLE ready_ = nullptr;
|
||||
HANDLE consumed_ = nullptr;
|
||||
HANDLE process_ = nullptr;
|
||||
protocol::Channel* channel_ = nullptr;
|
||||
std::byte* data_ = nullptr;
|
||||
bool alive_ = true;
|
||||
};
|
||||
|
||||
} // namespace t5de
|
||||
@@ -0,0 +1,108 @@
|
||||
#include "resources.hpp"
|
||||
|
||||
#include <mutex>
|
||||
#include <unordered_set>
|
||||
|
||||
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)
|
||||
: inner_(inner), recorder_(std::move(recorder)), id_(id)
|
||||
{
|
||||
add(static_cast<imvu::PiRenderTexture*>(this));
|
||||
}
|
||||
|
||||
Texture::~Texture()
|
||||
{
|
||||
remove(static_cast<imvu::PiRenderTexture*>(this));
|
||||
delete inner_; // through its virtual destructor: IMVU's code frees it
|
||||
recorder_->destroyTexture(id_);
|
||||
}
|
||||
|
||||
imvu::Vec2i Texture::size() const { return inner_->size(); }
|
||||
|
||||
void Texture::updateRegions(const imvu::Pixmap& pixmap, const imvu::CompoundRegion& regions)
|
||||
{
|
||||
inner_->updateRegions(pixmap, regions);
|
||||
recorder_->updateTexture(id_, pixmap); // all of it: the regions' layout isn't known
|
||||
}
|
||||
|
||||
void Texture::update(const imvu::Pixmap& pixmap)
|
||||
{
|
||||
inner_->update(pixmap);
|
||||
recorder_->updateTexture(id_, pixmap);
|
||||
}
|
||||
|
||||
imvu::Pixmap* Texture::read(imvu::Pixmap* result) const { return inner_->read(result); }
|
||||
|
||||
IndexBuffer::IndexBuffer(imvu::PiInternalIndexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
|
||||
: inner_(inner), recorder_(std::move(recorder)), id_(id)
|
||||
{
|
||||
add(static_cast<imvu::PiInternalIndexBuffer*>(this));
|
||||
}
|
||||
|
||||
IndexBuffer::~IndexBuffer()
|
||||
{
|
||||
remove(static_cast<imvu::PiInternalIndexBuffer*>(this));
|
||||
delete inner_;
|
||||
recorder_->destroyBuffer(id_);
|
||||
}
|
||||
|
||||
VertexBuffer::VertexBuffer(imvu::PiInternalVertexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
|
||||
: inner_(inner), recorder_(std::move(recorder)), id_(id)
|
||||
{
|
||||
add(static_cast<imvu::PiInternalVertexBuffer*>(this));
|
||||
}
|
||||
|
||||
VertexBuffer::~VertexBuffer()
|
||||
{
|
||||
remove(static_cast<imvu::PiInternalVertexBuffer*>(this));
|
||||
delete inner_;
|
||||
recorder_->destroyBuffer(id_);
|
||||
}
|
||||
|
||||
void VertexBuffer::update(const void* data, std::uint32_t stride, std::uint32_t elementSize, std::uint32_t elementCount)
|
||||
{
|
||||
inner_->update(data, stride, elementSize, elementCount);
|
||||
recorder_->updateBuffer(id_, data, stride, elementSize, elementCount);
|
||||
}
|
||||
|
||||
void VertexBuffer::unknown2() { inner_->unknown2(); }
|
||||
|
||||
} // namespace t5de
|
||||
@@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
// The textures and buffers the adapter's engine hands to the client: each wraps the client renderer's own (which the
|
||||
// client renderer still draws with) and mirrors it, by id, to the renderer process. The client deletes them through
|
||||
// their virtual destructors.
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "recorder.hpp"
|
||||
#include "t5de/imvu/resources.hpp"
|
||||
|
||||
namespace t5de {
|
||||
|
||||
class Texture final : public imvu::PiRenderTexture {
|
||||
public:
|
||||
Texture(imvu::PiRenderTexture* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id);
|
||||
~Texture() override;
|
||||
|
||||
imvu::Vec2i size() const override;
|
||||
void updateRegions(const imvu::Pixmap& pixmap, const imvu::CompoundRegion& regions) override;
|
||||
void update(const imvu::Pixmap& pixmap) override;
|
||||
imvu::Pixmap* read(imvu::Pixmap* result) const override;
|
||||
|
||||
[[nodiscard]] imvu::PiRenderTexture* inner() const { return inner_; }
|
||||
[[nodiscard]] std::uint32_t id() const { return id_; }
|
||||
|
||||
private:
|
||||
imvu::PiRenderTexture* inner_;
|
||||
std::shared_ptr<Recorder> recorder_;
|
||||
std::uint32_t id_;
|
||||
};
|
||||
|
||||
class IndexBuffer final : public imvu::PiInternalIndexBuffer {
|
||||
public:
|
||||
IndexBuffer(imvu::PiInternalIndexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id);
|
||||
~IndexBuffer() override;
|
||||
|
||||
[[nodiscard]] imvu::PiInternalIndexBuffer* inner() const { return inner_; }
|
||||
[[nodiscard]] std::uint32_t id() const { return id_; }
|
||||
|
||||
private:
|
||||
imvu::PiInternalIndexBuffer* inner_;
|
||||
std::shared_ptr<Recorder> recorder_;
|
||||
std::uint32_t id_;
|
||||
};
|
||||
|
||||
class VertexBuffer final : public imvu::PiInternalVertexBuffer {
|
||||
public:
|
||||
VertexBuffer(imvu::PiInternalVertexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id);
|
||||
~VertexBuffer() override;
|
||||
|
||||
void update(const void* data, std::uint32_t stride, std::uint32_t elementSize,
|
||||
std::uint32_t elementCount) override;
|
||||
void unknown2() override;
|
||||
|
||||
[[nodiscard]] imvu::PiInternalVertexBuffer* inner() const { return inner_; }
|
||||
[[nodiscard]] std::uint32_t id() const { return id_; }
|
||||
|
||||
private:
|
||||
imvu::PiInternalVertexBuffer* inner_;
|
||||
std::shared_ptr<Recorder> recorder_;
|
||||
std::uint32_t id_;
|
||||
};
|
||||
|
||||
// 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)
|
||||
{
|
||||
return ours(texture) ? static_cast<Texture*>(texture)->inner() : texture;
|
||||
}
|
||||
inline std::uint32_t id(imvu::PiRenderTexture* texture)
|
||||
{
|
||||
return ours(texture) ? static_cast<Texture*>(texture)->id() : 0;
|
||||
}
|
||||
inline imvu::PiInternalIndexBuffer* inner(imvu::PiInternalIndexBuffer* buffer)
|
||||
{
|
||||
return ours(buffer) ? static_cast<IndexBuffer*>(buffer)->inner() : buffer;
|
||||
}
|
||||
inline std::uint32_t id(imvu::PiInternalIndexBuffer* buffer)
|
||||
{
|
||||
return ours(buffer) ? static_cast<IndexBuffer*>(buffer)->id() : 0;
|
||||
}
|
||||
inline imvu::PiInternalVertexBuffer* inner(imvu::PiInternalVertexBuffer* buffer)
|
||||
{
|
||||
return ours(buffer) ? static_cast<VertexBuffer*>(buffer)->inner() : buffer;
|
||||
}
|
||||
inline std::uint32_t id(imvu::PiInternalVertexBuffer* buffer)
|
||||
{
|
||||
return ours(buffer) ? static_cast<VertexBuffer*>(buffer)->id() : 0;
|
||||
}
|
||||
|
||||
} // namespace t5de
|
||||
+39
-11
@@ -29,21 +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
|
||||
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
|
||||
|
||||
A spike: the adapter still has IMVU's Direct3D 9 engine draw the scene (the pass-through), and the renderer process
|
||||
only draws a box in the 3D view's corner, pulsing with the client's frames. Checked in the client (Direct3D 11 in
|
||||
software, on a machine without a graphics driver): the box draws over the scene from the 64-bit process, keeps in
|
||||
step with the client's frames, clicks on it reach the scene, and each renderer process exits with its engine (room
|
||||
changes) and with the client. The client keeps two engines alive in a room, so two processes (about 15 MB each);
|
||||
one process serving all of a client's engines would be leaner.
|
||||
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.
|
||||
|
||||
Next: the protocol carrying each frame's calls and the resources (buffers, textures), and the renderer process
|
||||
drawing the scene itself, with IMVU's Direct3D engine left as the fallback.
|
||||
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,
|
||||
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).
|
||||
- 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 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, and drawn by the client's renderer), and one renderer process for
|
||||
all of a client's views.
|
||||
|
||||
## Building
|
||||
|
||||
|
||||
+13
-1
@@ -71,7 +71,19 @@ 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,
|
||||
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.
|
||||
- Still unknown: point sprites (`drawPointSprites`, particles), render targets, `CompoundRegion` (texture update
|
||||
- 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
|
||||
Direct3D 9 point sprites: squares viewport height / distance pixels across (IMVU sets the size, 1, and the scale
|
||||
factors 0, 0, 1), textured over the whole square. Direct3D 9's documented 64-pixel default limit doesn't apply in
|
||||
practice: clamped to it they look much smaller and fainter than the client's.
|
||||
- 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
|
||||
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.
|
||||
- Memory crosses the boundary only through virtual methods (destructors, control blocks), so each side can use its
|
||||
|
||||
@@ -90,8 +90,21 @@ static_assert(sizeof(VertexAttributeArray<Vec3f>) == 12);
|
||||
// (width, height, pitch, rowBytes, pixels, ...).
|
||||
class Pixmap;
|
||||
|
||||
// Unknown so far: used only by drawPointSprites (particles), as a Visual C++ 2010 std::vector<PointSprite>.
|
||||
struct PointSpriteVector;
|
||||
// A particle (drawPointSprites): a point drawn as a camera-facing square, textured, about viewport height / distance
|
||||
// pixels across (1 to 64), as the Direct3D engine's point sprites are.
|
||||
struct PointSprite {
|
||||
Vec3f position;
|
||||
Color color;
|
||||
};
|
||||
|
||||
static_assert(sizeof(PointSprite) == 16);
|
||||
|
||||
// std::vector<PointSprite> as Visual C++ 2010 lays it out (release builds): its elements are [first, last).
|
||||
struct PointSpriteVector {
|
||||
const PointSprite* first;
|
||||
const PointSprite* last;
|
||||
const PointSprite* end; // of the allocation
|
||||
};
|
||||
|
||||
// Region lists IMVU passes to texture updates; unknown so far.
|
||||
class CompoundRegion;
|
||||
|
||||
@@ -7,19 +7,42 @@
|
||||
|
||||
namespace t5de::protocol {
|
||||
|
||||
inline constexpr std::uint32_t version = 1;
|
||||
inline constexpr std::uint32_t version = 3;
|
||||
|
||||
// A shared memory section the adapter creates for each engine. Its name is passed to the renderer process as
|
||||
// `--channel <name>`; the event "<name>-frame" (auto-reset) is set after each frame the client draws.
|
||||
// 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
|
||||
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>`
|
||||
// argument: this header, then `capacity` bytes of data. The adapter sends the command stream (commands.hpp) through
|
||||
// it in chunks: it waits for the event "<name>-consumed", writes a chunk and sets "<name>-ready"; the renderer
|
||||
// process copies the chunk out and sets "<name>-consumed" (both auto-reset; consumed starts set).
|
||||
struct Channel {
|
||||
std::uint32_t version;
|
||||
std::uint32_t closing; // nonzero: the engine is gone, the renderer process should exit
|
||||
std::uint64_t window; // IMVU's 3D window (an HWND; window handles are valid in every process)
|
||||
std::uint64_t frames; // frames the client has drawn
|
||||
View view;
|
||||
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) == 24, "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 wchar_t frameEventSuffix[] = L"-frame";
|
||||
inline constexpr std::uint32_t chunkEndsFrame = 1;
|
||||
inline constexpr std::uint32_t channelCapacity = 16 * 1024 * 1024;
|
||||
|
||||
inline constexpr wchar_t readyEventSuffix[] = L"-ready";
|
||||
inline constexpr wchar_t consumedEventSuffix[] = L"-consumed";
|
||||
|
||||
} // namespace t5de::protocol
|
||||
@@ -0,0 +1,266 @@
|
||||
#pragma once
|
||||
// The command stream: what the adapter records from IMVU's renderer interface, and the renderer process carries out.
|
||||
// Each command is a Header, its fixed-size body, then for some a variable-size payload (pixels, vertex data),
|
||||
// padded to 4 bytes. Built for 32-bit and 64-bit: fixed-size fields only.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace t5de::protocol {
|
||||
|
||||
enum class Op : std::uint16_t {
|
||||
BeginFrame = 1,
|
||||
EndFrame,
|
||||
Clear,
|
||||
SetProjection,
|
||||
SetModel,
|
||||
SetView,
|
||||
SetTextures,
|
||||
SetState,
|
||||
SetLight,
|
||||
SetFog,
|
||||
SetTextureTransform,
|
||||
CreateTexture,
|
||||
UpdateTexture,
|
||||
DestroyTexture,
|
||||
CreateBuffer,
|
||||
UpdateBuffer,
|
||||
DestroyBuffer,
|
||||
DrawTriangles,
|
||||
DrawArrays,
|
||||
DrawPointSprites,
|
||||
};
|
||||
|
||||
struct Header {
|
||||
Op op;
|
||||
std::uint16_t reserved;
|
||||
std::uint32_t size; // of the body and payload, padded to 4 bytes
|
||||
};
|
||||
|
||||
// Colors are 0xAARRGGBB. Resource ids are the adapter's, per engine; 0 means none.
|
||||
|
||||
struct BeginFrame {
|
||||
std::uint32_t offscreen; // drawn into a render target, not the window: nothing to show
|
||||
};
|
||||
|
||||
struct EndFrame {
|
||||
std::uint64_t sentAt; // the adapter's QueryPerformanceCounter (the same clock in every process)
|
||||
};
|
||||
|
||||
struct Clear {
|
||||
std::uint32_t flags; // 1: color, 2: depth (to 1)
|
||||
std::uint32_t color;
|
||||
};
|
||||
|
||||
// The viewport's size, and a Direct3D projection matrix (row-major, row vectors).
|
||||
struct SetProjection {
|
||||
std::int32_t width;
|
||||
std::int32_t height;
|
||||
float m[16];
|
||||
};
|
||||
|
||||
// An affine transform (SetModel, SetView): a 3x3 matrix, row-major, then the translation.
|
||||
struct SetTransform {
|
||||
float m[12];
|
||||
};
|
||||
|
||||
struct SetTextures {
|
||||
std::uint32_t texture0;
|
||||
std::uint32_t texture1;
|
||||
};
|
||||
|
||||
enum class State : std::uint32_t {
|
||||
DepthTest,
|
||||
DepthWrite,
|
||||
Blending,
|
||||
BlendMode, // 1: alpha, 2: additive, 3: premultiplied, 4: additive alpha
|
||||
AlphaTest,
|
||||
AlphaReference, // 0-255: pixels pass if their alpha is greater
|
||||
Culling,
|
||||
NormalInverting, // flips which winding faces front
|
||||
Lighting,
|
||||
Fog,
|
||||
Ambient, // a color
|
||||
};
|
||||
|
||||
struct SetState {
|
||||
State state;
|
||||
std::uint32_t value;
|
||||
};
|
||||
|
||||
struct SetLight {
|
||||
std::uint32_t index;
|
||||
std::uint32_t enabled;
|
||||
std::uint32_t color;
|
||||
std::uint32_t directional; // 0: a point light at vector; 1: light coming from direction vector
|
||||
float vector[3];
|
||||
};
|
||||
|
||||
struct SetFog {
|
||||
float start;
|
||||
float end;
|
||||
std::uint32_t color;
|
||||
};
|
||||
|
||||
// Texture coordinates for a stage: u' = u * m[0] + v * m[1] + m[4], v' = u * m[2] + v * m[3] + m[5].
|
||||
struct SetTextureTransform {
|
||||
std::uint32_t stage;
|
||||
std::uint32_t identity;
|
||||
float m[6];
|
||||
};
|
||||
|
||||
// Payload: width * height pixels (0xAARRGGBB), rows packed.
|
||||
struct CreateTexture {
|
||||
std::uint32_t id;
|
||||
std::uint32_t width;
|
||||
std::uint32_t height;
|
||||
std::uint32_t flags; // 2: wrap u, 4: wrap v (else clamp)
|
||||
};
|
||||
|
||||
struct UpdateTexture {
|
||||
std::uint32_t id;
|
||||
std::uint32_t width;
|
||||
std::uint32_t height;
|
||||
};
|
||||
|
||||
struct DestroyTexture {
|
||||
std::uint32_t id;
|
||||
};
|
||||
|
||||
enum class BufferKind : std::uint32_t {
|
||||
Index, // 16-bit indices
|
||||
Vertex, // positions and normals (3 floats), texture coordinates (2 floats) or colors (0xAARRGGBB)
|
||||
};
|
||||
|
||||
// Payload: count elements of elementSize bytes, packed; empty for a buffer filled later (UpdateBuffer).
|
||||
struct CreateBuffer {
|
||||
std::uint32_t id;
|
||||
BufferKind kind;
|
||||
std::uint32_t elementSize;
|
||||
std::uint32_t count;
|
||||
};
|
||||
|
||||
struct UpdateBuffer {
|
||||
std::uint32_t id;
|
||||
std::uint32_t elementSize;
|
||||
std::uint32_t count;
|
||||
};
|
||||
|
||||
struct DestroyBuffer {
|
||||
std::uint32_t id;
|
||||
};
|
||||
|
||||
// Triangles from buffers: indices and positions are required, the others optional.
|
||||
struct DrawTriangles {
|
||||
std::uint32_t indices;
|
||||
std::uint32_t positions;
|
||||
std::uint32_t normals;
|
||||
std::uint32_t texCoords;
|
||||
std::uint32_t colors;
|
||||
};
|
||||
|
||||
// Triangles from data in the payload, packed: indexCount 16-bit indices (padded to 4 bytes), then vertexCount
|
||||
// positions, and normals, texture coordinates and colors if present.
|
||||
struct DrawArrays {
|
||||
std::uint32_t vertexCount;
|
||||
std::uint32_t indexCount;
|
||||
std::uint32_t hasNormals;
|
||||
std::uint32_t hasTexCoords;
|
||||
std::uint32_t hasColors;
|
||||
};
|
||||
|
||||
// Particles: payload count sprites of 16 bytes, a position (3 floats) and a color. Each is a camera-facing square,
|
||||
// viewport height / distance pixels across (1 to 64), textured with the first texture over the whole square.
|
||||
struct DrawPointSprites {
|
||||
std::uint32_t count;
|
||||
};
|
||||
|
||||
static_assert(sizeof(Header) == 8 && sizeof(SetProjection) == 72 && sizeof(SetLight) == 28
|
||||
&& sizeof(DrawArrays) == 20);
|
||||
|
||||
inline constexpr std::size_t padded(std::size_t size) { return (size + 3) & ~std::size_t{3}; }
|
||||
|
||||
// Appends commands to a byte buffer.
|
||||
class Writer {
|
||||
public:
|
||||
// Appends a command and returns its payload, payloadSize bytes for the caller to fill.
|
||||
template <class Body>
|
||||
std::byte* add(Op op, const Body& body, std::size_t payloadSize = 0)
|
||||
{
|
||||
static_assert(sizeof(Body) % 4 == 0);
|
||||
const std::size_t size = sizeof(Body) + padded(payloadSize);
|
||||
const std::size_t start = bytes_.size();
|
||||
bytes_.resize(start + sizeof(Header) + size);
|
||||
const Header header{op, 0, static_cast<std::uint32_t>(size)};
|
||||
std::memcpy(bytes_.data() + start, &header, sizeof(Header));
|
||||
std::memcpy(bytes_.data() + start + sizeof(Header), &body, sizeof(Body));
|
||||
return bytes_.data() + start + sizeof(Header) + sizeof(Body);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::span<const std::byte> bytes() const { return bytes_; }
|
||||
[[nodiscard]] std::size_t size() const { return bytes_.size(); }
|
||||
void clear() { bytes_.clear(); }
|
||||
|
||||
private:
|
||||
std::vector<std::byte> bytes_;
|
||||
};
|
||||
|
||||
// One command, as read back.
|
||||
class Command {
|
||||
public:
|
||||
Command(Op op, std::span<const std::byte> data) : op_(op), data_(data) {}
|
||||
|
||||
[[nodiscard]] Op op() const { return op_; }
|
||||
|
||||
// The body, if the command is long enough to hold one.
|
||||
template <class Body>
|
||||
[[nodiscard]] std::optional<Body> body() const
|
||||
{
|
||||
if (data_.size() < sizeof(Body)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
Body body;
|
||||
std::memcpy(&body, data_.data(), sizeof(Body));
|
||||
return body;
|
||||
}
|
||||
|
||||
template <class Body>
|
||||
[[nodiscard]] std::span<const std::byte> payload() const
|
||||
{
|
||||
return data_.size() < sizeof(Body) ? std::span<const std::byte>{} : data_.subspan(sizeof(Body));
|
||||
}
|
||||
|
||||
private:
|
||||
Op op_;
|
||||
std::span<const std::byte> data_;
|
||||
};
|
||||
|
||||
// Reads commands back from a byte buffer; stops at the end, or at a truncated command.
|
||||
class Reader {
|
||||
public:
|
||||
explicit Reader(std::span<const std::byte> bytes) : bytes_(bytes) {}
|
||||
|
||||
std::optional<Command> next()
|
||||
{
|
||||
if (bytes_.size() < sizeof(Header)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
Header header;
|
||||
std::memcpy(&header, bytes_.data(), sizeof(Header));
|
||||
if (bytes_.size() - sizeof(Header) < header.size) {
|
||||
return std::nullopt;
|
||||
}
|
||||
Command command(header.op, bytes_.subspan(sizeof(Header), header.size));
|
||||
bytes_ = bytes_.subspan(sizeof(Header) + header.size);
|
||||
return command;
|
||||
}
|
||||
|
||||
private:
|
||||
std::span<const std::byte> bytes_;
|
||||
};
|
||||
|
||||
} // namespace t5de::protocol
|
||||
+14
-1
@@ -20,8 +20,21 @@ FetchContent_Declare(DiligentCore
|
||||
SYSTEM)
|
||||
FetchContent_MakeAvailable(DiligentCore)
|
||||
|
||||
add_executable(t5de_renderer WIN32 main.cpp)
|
||||
# Shader sources, embedded in the program (Diligent compiles them at run time): each shaders/<name>.hlsl becomes
|
||||
# <name>_hlsl.hpp, defining <camelCaseName>Hlsl.
|
||||
function(t5de_embed_shader file name)
|
||||
set(T5DE_SHADER_FILE ${file})
|
||||
set(T5DE_SHADER_NAME ${name})
|
||||
file(READ ${CMAKE_CURRENT_SOURCE_DIR}/shaders/${file} T5DE_SHADER_SOURCE)
|
||||
get_filename_component(stem ${file} NAME_WE)
|
||||
configure_file(shaders/embed.hpp.in ${CMAKE_CURRENT_BINARY_DIR}/generated/${stem}_hlsl.hpp @ONLY)
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS shaders/${file})
|
||||
endfunction()
|
||||
t5de_embed_shader(fixed_function.hlsl fixedFunctionHlsl)
|
||||
|
||||
add_executable(t5de_renderer WIN32 log.cpp main.cpp scene.cpp)
|
||||
set_target_properties(t5de_renderer PROPERTIES OUTPUT_NAME t5de-renderer)
|
||||
target_include_directories(t5de_renderer PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/generated)
|
||||
target_link_libraries(t5de_renderer PRIVATE t5de::protocol t5de::options Diligent-GraphicsEngineD3D11-static Diligent-Common)
|
||||
|
||||
install(TARGETS t5de_renderer RUNTIME DESTINATION .)
|
||||
@@ -0,0 +1,30 @@
|
||||
#include "log.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace t5de::log {
|
||||
|
||||
// Each line is one appending write, so lines from several processes don't mix.
|
||||
void write(std::string_view line)
|
||||
{
|
||||
static const HANDLE file = [] {
|
||||
wchar_t temp[MAX_PATH + 1];
|
||||
const DWORD length = GetTempPathW(MAX_PATH + 1, temp);
|
||||
if (length == 0 || length > MAX_PATH) {
|
||||
return INVALID_HANDLE_VALUE;
|
||||
}
|
||||
return CreateFileW((std::filesystem::path(temp) / L"t5de-renderer-process.log").c_str(), FILE_APPEND_DATA,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_ALWAYS,
|
||||
FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
}();
|
||||
if (file != INVALID_HANDLE_VALUE) {
|
||||
const std::string text = std::format("{}: {}\n", GetCurrentProcessId(), line);
|
||||
DWORD written = 0;
|
||||
WriteFile(file, text.data(), static_cast<DWORD>(text.size()), &written, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace t5de::log
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
// The renderer processes' log: %TEMP%\t5de-renderer-process.log, shared by all of them (lines start with the pid).
|
||||
|
||||
#include <format>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace t5de::log {
|
||||
|
||||
void write(std::string_view line);
|
||||
|
||||
template <class... Args>
|
||||
void info(std::format_string<Args...> format, Args&&... args)
|
||||
{
|
||||
write(std::format(format, std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
} // namespace t5de::log
|
||||
+213
-25
@@ -1,31 +1,45 @@
|
||||
// t5de-renderer.exe: the renderer process. Started by the adapter (t5deRenderEngine.dll) for each of the client's
|
||||
// 3D views; draws into a child window it places in the client's window, frame by frame as the client draws.
|
||||
//
|
||||
// For now a spike proving the architecture: a small box in the 3D view's corner, its color changing with each frame
|
||||
// the client draws, drawn with Diligent (Direct3D 11) from this 64-bit process.
|
||||
// 3D views: receives the client's drawing as commands through a shared memory channel, and draws it with Diligent
|
||||
// into a child window it places in the client's window (over the right half, for now, to compare with the client's
|
||||
// own renderer on the left).
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <d3d11.h>
|
||||
#include <dxgi.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "EngineFactoryD3D11.h"
|
||||
#include "RefCntAutoPtr.hpp"
|
||||
#include "RenderDeviceD3D11.h"
|
||||
#include "log.hpp"
|
||||
#include "scene.hpp"
|
||||
#include "t5de/protocol/channel.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace Diligent;
|
||||
using t5de::protocol::Channel;
|
||||
using t5de::protocol::View;
|
||||
|
||||
// The box: where in the client's 3D view, and how big.
|
||||
constexpr int boxX = 16;
|
||||
constexpr int boxY = 16;
|
||||
constexpr int boxWidth = 240;
|
||||
constexpr int boxHeight = 80;
|
||||
using t5de::log::info;
|
||||
|
||||
// Diligent's messages (shader compilation errors, invalid states, ...), into the log.
|
||||
void DILIGENT_CALL_TYPE diligent_message(DEBUG_MESSAGE_SEVERITY severity, const Char* message, const Char* function,
|
||||
const Char*, int)
|
||||
{
|
||||
static constexpr const char* severities[] = {"info", "warning", "error", "fatal error"};
|
||||
info("Diligent {}: {}{}{}", severities[severity < 4 ? severity : 3], function != nullptr ? function : "",
|
||||
function != nullptr ? ": " : "", message != nullptr ? message : "");
|
||||
}
|
||||
|
||||
struct Arguments {
|
||||
std::wstring channel;
|
||||
@@ -67,7 +81,93 @@ HWND create_child_window(HWND parent)
|
||||
.lpszClassName = L"T5DERenderer"};
|
||||
RegisterClassExW(&windowClass);
|
||||
return CreateWindowExW(0, windowClass.lpszClassName, L"", WS_CHILD | WS_VISIBLE | WS_DISABLED | WS_CLIPSIBLINGS,
|
||||
boxX, boxY, boxWidth, boxHeight, parent, nullptr, windowClass.hInstance, nullptr);
|
||||
0, 0, 1, 1, parent, nullptr, windowClass.hInstance, nullptr);
|
||||
}
|
||||
|
||||
// The view, as the adapter set it, or as %TEMP%\t5de-renderer-view.txt says while the client runs (off, split, full
|
||||
// or compare): for comparing the renderers without restarting the client. Read every second or so.
|
||||
View current_view(View adapter)
|
||||
{
|
||||
static View view = adapter;
|
||||
static ULONGLONG checked = 0;
|
||||
if (GetTickCount64() - checked < 1000) {
|
||||
return view;
|
||||
}
|
||||
checked = GetTickCount64();
|
||||
view = adapter;
|
||||
wchar_t temp[MAX_PATH + 1];
|
||||
const DWORD length = GetTempPathW(MAX_PATH + 1, temp);
|
||||
if (length == 0 || length > MAX_PATH) {
|
||||
return view;
|
||||
}
|
||||
const HANDLE file = CreateFileW((std::filesystem::path(temp) / L"t5de-renderer-view.txt").c_str(), GENERIC_READ,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (file == INVALID_HANDLE_VALUE) {
|
||||
return view;
|
||||
}
|
||||
char text[16] = {};
|
||||
DWORD read = 0;
|
||||
ReadFile(file, text, sizeof(text) - 1, &read, nullptr);
|
||||
CloseHandle(file);
|
||||
const std::string_view setting(text, std::strcspn(text, "\r\n "));
|
||||
view = setting == "off" ? View::Off
|
||||
: setting == "split" ? View::Split
|
||||
: setting == "full" ? View::Full
|
||||
: setting == "compare" ? View::Compare
|
||||
: adapter;
|
||||
return view;
|
||||
}
|
||||
|
||||
// Shader changes to try while the client runs: %TEMP%\t5de-renderer-shaders\fixed_function.hlsl, if there is
|
||||
// one, replaces the built-in shaders, and is read again when it changes (checked every second or so).
|
||||
void reload_shaders(t5de::Scene& scene)
|
||||
{
|
||||
static ULONGLONG checked = 0;
|
||||
static std::filesystem::file_time_type loaded{};
|
||||
if (GetTickCount64() - checked < 1000) {
|
||||
return;
|
||||
}
|
||||
checked = GetTickCount64();
|
||||
std::error_code error;
|
||||
const auto path = std::filesystem::temp_directory_path(error) / L"t5de-renderer-shaders" / L"fixed_function.hlsl";
|
||||
const auto modified = std::filesystem::last_write_time(path, error);
|
||||
if (error || modified == loaded) {
|
||||
return;
|
||||
}
|
||||
const HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
|
||||
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (file == INVALID_HANDLE_VALUE) {
|
||||
return;
|
||||
}
|
||||
std::string source(static_cast<std::size_t>(std::filesystem::file_size(path, error)), ' ');
|
||||
DWORD read = 0;
|
||||
ReadFile(file, source.data(), static_cast<DWORD>(source.size()), &read, nullptr);
|
||||
CloseHandle(file);
|
||||
source.resize(read);
|
||||
loaded = modified;
|
||||
scene.setShaderSource(std::move(source));
|
||||
info("shaders reloaded from {}", path.string());
|
||||
}
|
||||
|
||||
// Where the child window goes in the client's 3D view, and where in the 3D view its scene starts (offset).
|
||||
struct Placement {
|
||||
RECT window{};
|
||||
int offset = 0;
|
||||
};
|
||||
|
||||
Placement placement(HWND parent, View view)
|
||||
{
|
||||
Placement place;
|
||||
GetClientRect(parent, &place.window);
|
||||
if (view == View::Split || view == View::Compare) {
|
||||
place.window.left = place.window.right / 2;
|
||||
}
|
||||
place.offset = view == View::Compare ? 0 : place.window.left;
|
||||
if (view == View::Off) {
|
||||
place.window = {};
|
||||
}
|
||||
return place;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -80,37 +180,74 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
|
||||
}
|
||||
HANDLE mapping = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, arguments.channel.c_str());
|
||||
auto* channel = mapping == nullptr ? nullptr
|
||||
: static_cast<t5de::protocol::Channel*>(MapViewOfFile(
|
||||
mapping, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(t5de::protocol::Channel)));
|
||||
HANDLE frameEvent = OpenEventW(SYNCHRONIZE, FALSE, (arguments.channel + t5de::protocol::frameEventSuffix).c_str());
|
||||
: static_cast<Channel*>(MapViewOfFile(mapping, FILE_MAP_ALL_ACCESS, 0, 0, 0));
|
||||
HANDLE ready = OpenEventW(SYNCHRONIZE, FALSE, (arguments.channel + t5de::protocol::readyEventSuffix).c_str());
|
||||
HANDLE consumed =
|
||||
OpenEventW(EVENT_MODIFY_STATE, FALSE, (arguments.channel + t5de::protocol::consumedEventSuffix).c_str());
|
||||
HANDLE parent = OpenProcess(SYNCHRONIZE, FALSE, arguments.parent);
|
||||
if (channel == nullptr || frameEvent == nullptr || parent == nullptr || channel->version != t5de::protocol::version) {
|
||||
if (channel == nullptr || ready == nullptr || consumed == nullptr || parent == nullptr
|
||||
|| channel->version != t5de::protocol::version) {
|
||||
return 3;
|
||||
}
|
||||
const auto* data = reinterpret_cast<const std::byte*>(channel + 1);
|
||||
|
||||
HWND window = create_child_window(reinterpret_cast<HWND>(static_cast<std::uintptr_t>(channel->window)));
|
||||
const HWND clientWindow = reinterpret_cast<HWND>(static_cast<std::uintptr_t>(channel->window));
|
||||
const HWND window = create_child_window(clientWindow);
|
||||
if (window == nullptr) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
info("started for window {:#x}, view {}", channel->window, static_cast<int>(channel->view));
|
||||
IEngineFactoryD3D11* factory = GetEngineFactoryD3D11();
|
||||
factory->SetMessageCallback(diligent_message);
|
||||
RefCntAutoPtr<IRenderDevice> device;
|
||||
RefCntAutoPtr<IDeviceContext> context;
|
||||
EngineD3D11CreateInfo engineInfo;
|
||||
factory->CreateDeviceAndContextsD3D11(engineInfo, &device, &context);
|
||||
if (!device) {
|
||||
info("can't create a Direct3D 11 device");
|
||||
return 5;
|
||||
}
|
||||
// Show each frame as soon as it's drawn: Direct3D 11 otherwise lets 3 frames queue, which the view would trail by.
|
||||
if (RefCntAutoPtr<IRenderDeviceD3D11> d3d11{device, IID_RenderDeviceD3D11}) {
|
||||
IDXGIDevice1* dxgi = nullptr;
|
||||
if (SUCCEEDED(d3d11->GetD3D11Device()->QueryInterface(__uuidof(IDXGIDevice1), reinterpret_cast<void**>(&dxgi)))) {
|
||||
dxgi->SetMaximumFrameLatency(1);
|
||||
dxgi->Release();
|
||||
}
|
||||
}
|
||||
RefCntAutoPtr<ISwapChain> swapChain;
|
||||
SwapChainDesc swapChainDesc;
|
||||
swapChainDesc.ColorBufferFormat = TEX_FORMAT_RGBA8_UNORM; // no sRGB conversion, like the client's renderer
|
||||
swapChainDesc.DepthBufferFormat = TEX_FORMAT_D32_FLOAT;
|
||||
factory->CreateSwapChainD3D11(device, context, swapChainDesc, FullScreenModeDesc{}, Win32NativeWindow{window},
|
||||
&swapChain);
|
||||
if (!swapChain) {
|
||||
info("can't create a swap chain");
|
||||
return 6;
|
||||
}
|
||||
t5de::Scene scene(device, context, swapChain);
|
||||
|
||||
// Takes the chunk waiting in the channel (after "ready"), and lets the adapter send the next: appends it to
|
||||
// frame, and returns whether it ends a frame.
|
||||
std::vector<std::byte> frame;
|
||||
const auto take_chunk = [&] {
|
||||
const std::uint32_t size = channel->chunkSize <= channel->capacity ? channel->chunkSize : 0;
|
||||
const bool endsFrame = (channel->chunkFlags & t5de::protocol::chunkEndsFrame) != 0;
|
||||
frame.insert(frame.end(), data, data + size);
|
||||
SetEvent(consumed);
|
||||
return endsFrame;
|
||||
};
|
||||
|
||||
LARGE_INTEGER frequency;
|
||||
QueryPerformanceFrequency(&frequency);
|
||||
double drawTime = 0;
|
||||
double latency = 0;
|
||||
int drawn = 0;
|
||||
int skipped = 0;
|
||||
RECT placed{};
|
||||
for (;;) {
|
||||
const HANDLE handles[] = {frameEvent, parent};
|
||||
const HANDLE handles[] = {ready, parent};
|
||||
const DWORD result = MsgWaitForMultipleObjects(2, handles, FALSE, 1000, QS_ALLINPUT);
|
||||
MSG message;
|
||||
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
||||
@@ -119,19 +256,70 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
|
||||
if (result == WAIT_OBJECT_0 + 1 || channel->closing != 0) {
|
||||
break; // the client exited, or the engine is gone
|
||||
}
|
||||
if (result != WAIT_OBJECT_0) {
|
||||
if (result != WAIT_OBJECT_0 || !take_chunk()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// T5DE gold, pulsing with the client's frames.
|
||||
const float pulse = 0.5f + 0.5f * std::sin(static_cast<float>(channel->frames) * 0.1f);
|
||||
const float color[] = {1.0f, 0.82f * pulse + 0.1f, 0.0f, 1.0f};
|
||||
ITextureView* backBuffer = swapChain->GetCurrentBackBufferRTV();
|
||||
context->SetRenderTargets(1, &backBuffer, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||
context->ClearRenderTarget(backBuffer, color, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||
swapChain->Present(0);
|
||||
// If the client has sent more frames meanwhile (drawing fell behind), catch up: carry out the older ones'
|
||||
// changes without drawing them, and draw only the newest. (An incomplete frame stays for the next round.)
|
||||
std::size_t complete = frame.size();
|
||||
while (WaitForSingleObject(ready, 0) == WAIT_OBJECT_0) {
|
||||
bool ended = take_chunk();
|
||||
while (!ended && WaitForSingleObject(ready, 1000) == WAIT_OBJECT_0) {
|
||||
ended = take_chunk();
|
||||
}
|
||||
if (!ended) {
|
||||
break;
|
||||
}
|
||||
scene.execute(std::span(frame).first(complete), 0, false);
|
||||
frame.erase(frame.begin(), frame.begin() + static_cast<std::ptrdiff_t>(complete));
|
||||
complete = frame.size();
|
||||
++skipped;
|
||||
}
|
||||
|
||||
// Follow the client's 3D view's size.
|
||||
const auto [place, offset] = placement(clientWindow, current_view(channel->view));
|
||||
const int width = place.right - place.left;
|
||||
const int height = place.bottom - place.top;
|
||||
if (std::memcmp(&place, &placed, sizeof(RECT)) != 0) {
|
||||
placed = place;
|
||||
if (width > 0 && height > 0) {
|
||||
SetWindowPos(window, nullptr, place.left, place.top, width, height,
|
||||
SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
||||
swapChain->Resize(static_cast<Uint32>(width), static_cast<Uint32>(height));
|
||||
info("drawing {}x{} at {} in the client's 3D view, its scene from {}", width, height, place.left, offset);
|
||||
} else {
|
||||
ShowWindow(window, SW_HIDE);
|
||||
channel->covering = 0;
|
||||
info("hidden");
|
||||
}
|
||||
}
|
||||
reload_shaders(scene);
|
||||
LARGE_INTEGER start, end;
|
||||
QueryPerformanceCounter(&start);
|
||||
const bool covers = current_view(channel->view) == View::Full && width > 0 && height > 0;
|
||||
if (scene.execute(std::span(frame).first(complete), offset, width > 0 && height > 0)) {
|
||||
swapChain->Present(0);
|
||||
channel->covering = covers ? 1 : 0; // the client's renderer can stop drawing (or must start again)
|
||||
QueryPerformanceCounter(&end);
|
||||
drawTime += static_cast<double>(end.QuadPart - start.QuadPart) / static_cast<double>(frequency.QuadPart);
|
||||
latency += static_cast<double>(end.QuadPart - static_cast<LONGLONG>(scene.lastSentAt())) /
|
||||
static_cast<double>(frequency.QuadPart);
|
||||
if (++drawn == 300) {
|
||||
info("last 300 frames: {:.1f} ms each to draw and present, {:.1f} ms from the client's end of frame to "
|
||||
"presented, {} skipped to catch up",
|
||||
drawTime * 1000 / drawn, latency * 1000 / drawn, skipped);
|
||||
drawTime = 0;
|
||||
latency = 0;
|
||||
drawn = 0;
|
||||
skipped = 0;
|
||||
}
|
||||
}
|
||||
frame.erase(frame.begin(), frame.begin() + static_cast<std::ptrdiff_t>(complete));
|
||||
}
|
||||
|
||||
channel->covering = 0;
|
||||
info("exiting");
|
||||
DestroyWindow(window);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,687 @@
|
||||
#include "scene.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "fixed_function_hlsl.hpp"
|
||||
|
||||
namespace t5de {
|
||||
|
||||
using namespace Diligent;
|
||||
using protocol::Op;
|
||||
|
||||
namespace {
|
||||
|
||||
void set_identity(float (&m)[16])
|
||||
{
|
||||
std::memset(m, 0, sizeof(m));
|
||||
m[0] = m[5] = m[10] = m[15] = 1;
|
||||
}
|
||||
|
||||
// An affine transform (3x3 row-major, then translation) as a 4x4 matrix for row vectors.
|
||||
void set_transform(float (&m)[16], const float (&t)[12])
|
||||
{
|
||||
for (int row = 0; row < 3; ++row) {
|
||||
m[row * 4 + 0] = t[row * 3 + 0];
|
||||
m[row * 4 + 1] = t[row * 3 + 1];
|
||||
m[row * 4 + 2] = t[row * 3 + 2];
|
||||
m[row * 4 + 3] = 0;
|
||||
}
|
||||
m[12] = t[9];
|
||||
m[13] = t[10];
|
||||
m[14] = t[11];
|
||||
m[15] = 1;
|
||||
}
|
||||
|
||||
// u' = u * m[0] + v * m[1] + m[4], v' = u * m[2] + v * m[3] + m[5], as two rows dotted with (u, v, 1).
|
||||
void set_texture_transform(float (&rows)[8], const float (&m)[6])
|
||||
{
|
||||
const float values[8] = {m[0], m[1], m[4], 0, m[2], m[3], m[5], 0};
|
||||
std::memcpy(rows, values, sizeof(rows));
|
||||
}
|
||||
|
||||
void set_color(float (&rgba)[4], std::uint32_t color)
|
||||
{
|
||||
rgba[0] = static_cast<float>((color >> 16) & 0xff) / 255.0f;
|
||||
rgba[1] = static_cast<float>((color >> 8) & 0xff) / 255.0f;
|
||||
rgba[2] = static_cast<float>(color & 0xff) / 255.0f;
|
||||
rgba[3] = static_cast<float>(color >> 24) / 255.0f;
|
||||
}
|
||||
|
||||
constexpr std::uint32_t hasNormals = 1;
|
||||
constexpr std::uint32_t hasTexCoords = 2;
|
||||
constexpr std::uint32_t hasColors = 4;
|
||||
|
||||
} // namespace
|
||||
|
||||
Scene::Scene(IRenderDevice* device, IDeviceContext* context, ISwapChain* swapChain)
|
||||
: device_(device), context_(context), swapChain_(swapChain), shaderSource_(fixedFunctionHlsl)
|
||||
{
|
||||
BufferDesc constants;
|
||||
constants.Name = "Constants";
|
||||
constants.Size = sizeof(Constants);
|
||||
constants.Usage = USAGE_DYNAMIC;
|
||||
constants.BindFlags = BIND_UNIFORM_BUFFER;
|
||||
constants.CPUAccessFlags = CPU_ACCESS_WRITE;
|
||||
device_->CreateBuffer(constants, nullptr, &constantBuffer_);
|
||||
|
||||
// IMVU's textures: bilinear, no mipmaps, wrapped or clamped per texture.
|
||||
for (int wrap = 0; wrap < 4; ++wrap) {
|
||||
SamplerDesc sampler;
|
||||
sampler.MinFilter = FILTER_TYPE_LINEAR;
|
||||
sampler.MagFilter = FILTER_TYPE_LINEAR;
|
||||
sampler.MipFilter = FILTER_TYPE_POINT;
|
||||
sampler.AddressU = (wrap & 1) != 0 ? TEXTURE_ADDRESS_WRAP : TEXTURE_ADDRESS_CLAMP;
|
||||
sampler.AddressV = (wrap & 2) != 0 ? TEXTURE_ADDRESS_WRAP : TEXTURE_ADDRESS_CLAMP;
|
||||
device_->CreateSampler(sampler, &samplers_[wrap]);
|
||||
}
|
||||
|
||||
set_identity(constants_.world);
|
||||
set_identity(constants_.view);
|
||||
set_identity(constants_.projection);
|
||||
const float identity[6] = {1, 0, 0, 1, 0, 0};
|
||||
set_texture_transform(constants_.texTransform0, identity);
|
||||
set_texture_transform(constants_.texTransform1, identity);
|
||||
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)
|
||||
{
|
||||
offsetX_ = offsetX;
|
||||
draw_ = draw;
|
||||
bool drew = false;
|
||||
protocol::Reader reader(commands);
|
||||
while (const auto command = reader.next()) {
|
||||
switch (command->op()) {
|
||||
case Op::BeginFrame:
|
||||
if (const auto begin = command->body<protocol::BeginFrame>()) {
|
||||
onscreen_ = begin->offscreen == 0 && draw_;
|
||||
if (onscreen_) {
|
||||
ITextureView* target = swapChain_->GetCurrentBackBufferRTV();
|
||||
context_->SetRenderTargets(1, &target, swapChain_->GetDepthBufferDSV(),
|
||||
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Op::EndFrame:
|
||||
if (const auto end = command->body<protocol::EndFrame>()) {
|
||||
lastSentAt_ = end->sentAt;
|
||||
}
|
||||
drew = drew || onscreen_;
|
||||
onscreen_ = false;
|
||||
break;
|
||||
case Op::Clear:
|
||||
if (const auto clear = command->body<protocol::Clear>(); clear && onscreen_) {
|
||||
if ((clear->flags & 1) != 0) {
|
||||
float color[4];
|
||||
set_color(color, clear->color);
|
||||
context_->ClearRenderTarget(swapChain_->GetCurrentBackBufferRTV(), color,
|
||||
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||
}
|
||||
if ((clear->flags & 2) != 0) {
|
||||
context_->ClearDepthStencil(swapChain_->GetDepthBufferDSV(), CLEAR_DEPTH_FLAG, 1.0f, 0,
|
||||
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Op::SetProjection:
|
||||
if (const auto projection = command->body<protocol::SetProjection>()) {
|
||||
setProjection(*projection);
|
||||
}
|
||||
break;
|
||||
case Op::SetModel:
|
||||
if (const auto model = command->body<protocol::SetTransform>()) {
|
||||
set_transform(constants_.world, model->m);
|
||||
}
|
||||
break;
|
||||
case Op::SetView:
|
||||
if (const auto view = command->body<protocol::SetTransform>()) {
|
||||
set_transform(constants_.view, view->m);
|
||||
}
|
||||
break;
|
||||
case Op::SetTextures:
|
||||
if (const auto textures = command->body<protocol::SetTextures>()) {
|
||||
state_.texture0 = textures->texture0;
|
||||
state_.texture1 = textures->texture1;
|
||||
}
|
||||
break;
|
||||
case Op::SetState:
|
||||
if (const auto state = command->body<protocol::SetState>()) {
|
||||
setState(*state);
|
||||
}
|
||||
break;
|
||||
case Op::SetLight:
|
||||
if (const auto light = command->body<protocol::SetLight>(); light && light->index < 8) {
|
||||
set_color(constants_.lightColor[light->index], light->color);
|
||||
constants_.lightColor[light->index][3] = light->enabled != 0 ? 1.0f : 0.0f;
|
||||
std::memcpy(constants_.lightVector[light->index], light->vector, sizeof(light->vector));
|
||||
constants_.lightVector[light->index][3] = light->directional != 0 ? 1.0f : 0.0f;
|
||||
}
|
||||
break;
|
||||
case Op::SetFog:
|
||||
if (const auto fog = command->body<protocol::SetFog>()) {
|
||||
constants_.fog[0] = fog->start;
|
||||
constants_.fog[1] = fog->end;
|
||||
set_color(constants_.fogColor, fog->color);
|
||||
}
|
||||
break;
|
||||
case Op::SetTextureTransform:
|
||||
if (const auto transform = command->body<protocol::SetTextureTransform>(); transform && transform->stage < 2) {
|
||||
set_texture_transform(transform->stage == 0 ? constants_.texTransform0 : constants_.texTransform1,
|
||||
transform->m);
|
||||
}
|
||||
break;
|
||||
case Op::CreateTexture:
|
||||
createTexture(*command);
|
||||
break;
|
||||
case Op::UpdateTexture:
|
||||
updateTexture(*command);
|
||||
break;
|
||||
case Op::DestroyTexture:
|
||||
if (const auto destroy = command->body<protocol::DestroyTexture>()) {
|
||||
textures_.erase(destroy->id);
|
||||
}
|
||||
break;
|
||||
case Op::CreateBuffer:
|
||||
createBuffer(*command);
|
||||
break;
|
||||
case Op::UpdateBuffer:
|
||||
updateBuffer(*command);
|
||||
break;
|
||||
case Op::DestroyBuffer:
|
||||
if (const auto destroy = command->body<protocol::DestroyBuffer>()) {
|
||||
buffers_.erase(destroy->id);
|
||||
}
|
||||
break;
|
||||
case Op::DrawTriangles:
|
||||
if (const auto triangles = command->body<protocol::DrawTriangles>(); triangles && onscreen_) {
|
||||
drawTriangles(*triangles);
|
||||
}
|
||||
break;
|
||||
case Op::DrawArrays:
|
||||
if (onscreen_) {
|
||||
drawArrays(*command);
|
||||
}
|
||||
break;
|
||||
case Op::DrawPointSprites:
|
||||
if (onscreen_) {
|
||||
drawPointSprites(*command);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return drew;
|
||||
}
|
||||
|
||||
void Scene::setProjection(const protocol::SetProjection& 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[1] = static_cast<float>(projection.height);
|
||||
if (!onscreen_) {
|
||||
return;
|
||||
}
|
||||
// The client's projection covers its whole 3D view; the window shows part of it, from offsetX.
|
||||
const SwapChainDesc& window = swapChain_->GetDesc();
|
||||
Viewport viewport;
|
||||
viewport.TopLeftX = static_cast<float>(-offsetX_);
|
||||
viewport.TopLeftY = 0;
|
||||
viewport.Width = static_cast<float>(projection.width);
|
||||
viewport.Height = static_cast<float>(projection.height);
|
||||
viewport.MinDepth = 0;
|
||||
viewport.MaxDepth = 1;
|
||||
context_->SetViewports(1, &viewport, window.Width, window.Height);
|
||||
}
|
||||
|
||||
void Scene::setState(const protocol::SetState& state)
|
||||
{
|
||||
const bool on = state.value != 0;
|
||||
switch (state.state) {
|
||||
case protocol::State::DepthTest:
|
||||
state_.depthTest = on;
|
||||
break;
|
||||
case protocol::State::DepthWrite:
|
||||
state_.depthWrite = on;
|
||||
break;
|
||||
case protocol::State::Blending:
|
||||
state_.blending = on;
|
||||
break;
|
||||
case protocol::State::BlendMode:
|
||||
if (state.value >= 1 && state.value <= 4) {
|
||||
state_.blendMode = state.value;
|
||||
}
|
||||
break;
|
||||
case protocol::State::AlphaTest:
|
||||
constants_.flags[1] = on ? 1.0f : 0.0f;
|
||||
break;
|
||||
case protocol::State::AlphaReference:
|
||||
constants_.flags[2] = static_cast<float>(state.value);
|
||||
break;
|
||||
case protocol::State::Culling:
|
||||
state_.culling = on;
|
||||
break;
|
||||
case protocol::State::NormalInverting:
|
||||
state_.normalInverting = on;
|
||||
break;
|
||||
case protocol::State::Lighting:
|
||||
constants_.flags[0] = on ? 1.0f : 0.0f;
|
||||
break;
|
||||
case protocol::State::Fog:
|
||||
constants_.fog[2] = on ? 1.0f : 0.0f;
|
||||
break;
|
||||
case protocol::State::Ambient:
|
||||
set_color(constants_.ambient, state.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Scene::createTexture(const protocol::Command& command)
|
||||
{
|
||||
const auto create = command.body<protocol::CreateTexture>();
|
||||
const auto pixels = command.payload<protocol::CreateTexture>();
|
||||
if (!create || create->width == 0 || create->height == 0
|
||||
|| pixels.size() < std::size_t{create->width} * create->height * 4) {
|
||||
return;
|
||||
}
|
||||
TextureDesc desc;
|
||||
desc.Name = "Client texture";
|
||||
desc.Type = RESOURCE_DIM_TEX_2D;
|
||||
desc.Width = create->width;
|
||||
desc.Height = create->height;
|
||||
desc.MipLevels = 1;
|
||||
desc.Format = TEX_FORMAT_BGRA8_UNORM; // 0xAARRGGBB in memory
|
||||
desc.BindFlags = BIND_SHADER_RESOURCE;
|
||||
desc.Usage = USAGE_DEFAULT;
|
||||
TextureSubResData level;
|
||||
level.pData = pixels.data();
|
||||
level.Stride = std::uint64_t{create->width} * 4;
|
||||
TextureData data;
|
||||
data.pSubResources = &level;
|
||||
data.NumSubresources = 1;
|
||||
Texture texture{.width = create->width, .height = create->height, .flags = create->flags};
|
||||
device_->CreateTexture(desc, &data, &texture.texture);
|
||||
if (!texture.texture) {
|
||||
return;
|
||||
}
|
||||
texture.texture->GetDefaultView(TEXTURE_VIEW_SHADER_RESOURCE)
|
||||
->SetSampler(samplers_[((create->flags & 2) != 0 ? 1 : 0) | ((create->flags & 4) != 0 ? 2 : 0)]);
|
||||
textures_[create->id] = std::move(texture);
|
||||
}
|
||||
|
||||
void Scene::updateTexture(const protocol::Command& command)
|
||||
{
|
||||
const auto update = command.body<protocol::UpdateTexture>();
|
||||
const auto pixels = command.payload<protocol::UpdateTexture>();
|
||||
if (!update || pixels.size() < std::size_t{update->width} * update->height * 4) {
|
||||
return;
|
||||
}
|
||||
const auto found = textures_.find(update->id);
|
||||
if (found == textures_.end()) {
|
||||
return;
|
||||
}
|
||||
Texture& texture = found->second;
|
||||
if (texture.width != update->width || texture.height != update->height) {
|
||||
// A different size: make it again, keeping its sampling.
|
||||
protocol::Writer writer;
|
||||
std::memcpy(writer.add(Op::CreateTexture,
|
||||
protocol::CreateTexture{update->id, update->width, update->height, texture.flags},
|
||||
pixels.size()),
|
||||
pixels.data(), pixels.size());
|
||||
protocol::Reader reader(writer.bytes());
|
||||
createTexture(*reader.next());
|
||||
return;
|
||||
}
|
||||
Box box;
|
||||
box.MaxX = update->width;
|
||||
box.MaxY = update->height;
|
||||
TextureSubResData level;
|
||||
level.pData = pixels.data();
|
||||
level.Stride = std::uint64_t{update->width} * 4;
|
||||
context_->UpdateTexture(texture.texture, 0, 0, box, level, RESOURCE_STATE_TRANSITION_MODE_TRANSITION,
|
||||
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||
}
|
||||
|
||||
void Scene::createBuffer(const protocol::Command& command)
|
||||
{
|
||||
const auto create = command.body<protocol::CreateBuffer>();
|
||||
if (!create || create->count == 0 || create->elementSize == 0) {
|
||||
return;
|
||||
}
|
||||
const std::size_t size = std::size_t{create->elementSize} * create->count;
|
||||
const auto contents = command.payload<protocol::CreateBuffer>();
|
||||
const bool filled = contents.size() >= size;
|
||||
BufferDesc desc;
|
||||
desc.Name = create->kind == protocol::BufferKind::Index ? "Client indices" : "Client vertices";
|
||||
desc.Size = size;
|
||||
desc.BindFlags = create->kind == protocol::BufferKind::Index ? BIND_INDEX_BUFFER : BIND_VERTEX_BUFFER;
|
||||
desc.Usage = filled ? USAGE_IMMUTABLE : USAGE_DEFAULT; // dynamic buffers are filled by updates
|
||||
BufferData data;
|
||||
data.pData = contents.data();
|
||||
data.DataSize = size;
|
||||
Buffer buffer{.kind = create->kind, .elementSize = create->elementSize, .count = create->count};
|
||||
device_->CreateBuffer(desc, filled ? &data : nullptr, &buffer.buffer);
|
||||
if (buffer.buffer) {
|
||||
buffers_[create->id] = std::move(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
void Scene::updateBuffer(const protocol::Command& command)
|
||||
{
|
||||
const auto update = command.body<protocol::UpdateBuffer>();
|
||||
const auto contents = command.payload<protocol::UpdateBuffer>();
|
||||
if (!update) {
|
||||
return;
|
||||
}
|
||||
const auto found = buffers_.find(update->id);
|
||||
const std::size_t size = std::size_t{update->elementSize} * update->count;
|
||||
if (found == buffers_.end() || contents.size() < size || found->second.elementSize != update->elementSize
|
||||
|| found->second.count != update->count || found->second.buffer->GetDesc().Usage != USAGE_DEFAULT) {
|
||||
return;
|
||||
}
|
||||
context_->UpdateBuffer(found->second.buffer, 0, size, contents.data(), RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||
}
|
||||
|
||||
ITextureView* Scene::textureView(std::uint32_t id)
|
||||
{
|
||||
const auto found = textures_.find(id);
|
||||
return found == textures_.end() ? nullptr : found->second.texture->GetDefaultView(TEXTURE_VIEW_SHADER_RESOURCE);
|
||||
}
|
||||
|
||||
void Scene::drawTriangles(const protocol::DrawTriangles& draw)
|
||||
{
|
||||
const auto buffer = [this](std::uint32_t id, protocol::BufferKind kind, std::uint32_t elementSize) -> Buffer* {
|
||||
const auto found = buffers_.find(id);
|
||||
if (id == 0 || found == buffers_.end() || found->second.kind != kind || found->second.elementSize != elementSize) {
|
||||
return nullptr;
|
||||
}
|
||||
return &found->second;
|
||||
};
|
||||
Buffer* indices = buffer(draw.indices, protocol::BufferKind::Index, 2);
|
||||
Buffer* positions = buffer(draw.positions, protocol::BufferKind::Vertex, 12);
|
||||
if (indices == nullptr || positions == nullptr) {
|
||||
return;
|
||||
}
|
||||
// Optional streams count only if they cover every vertex.
|
||||
const auto optional = [&](std::uint32_t id, std::uint32_t elementSize) -> IBuffer* {
|
||||
Buffer* found = buffer(id, protocol::BufferKind::Vertex, elementSize);
|
||||
return found != nullptr && found->count >= positions->count ? found->buffer.RawPtr() : nullptr;
|
||||
};
|
||||
this->draw(indices->buffer, indices->count, positions->buffer, optional(draw.normals, 12),
|
||||
optional(draw.texCoords, 8), optional(draw.colors, 4));
|
||||
}
|
||||
|
||||
void Scene::drawArrays(const protocol::Command& command)
|
||||
{
|
||||
const auto arrays = command.body<protocol::DrawArrays>();
|
||||
const auto payload = command.payload<protocol::DrawArrays>();
|
||||
if (!arrays || arrays->vertexCount == 0 || arrays->indexCount == 0) {
|
||||
return;
|
||||
}
|
||||
const std::size_t indexBytes = protocol::padded(std::size_t{arrays->indexCount} * 2);
|
||||
const std::size_t vertices = arrays->vertexCount;
|
||||
if (payload.size() < indexBytes + vertices * (12 + (arrays->hasNormals ? 12 : 0) + (arrays->hasTexCoords ? 8 : 0) +
|
||||
(arrays->hasColors ? 4 : 0))) {
|
||||
return;
|
||||
}
|
||||
const auto make = [this](const std::byte* data, std::size_t size, BIND_FLAGS bind) {
|
||||
BufferDesc desc;
|
||||
desc.Name = "Client arrays";
|
||||
desc.Size = size;
|
||||
desc.BindFlags = bind;
|
||||
desc.Usage = USAGE_IMMUTABLE;
|
||||
BufferData contents;
|
||||
contents.pData = data;
|
||||
contents.DataSize = size;
|
||||
RefCntAutoPtr<IBuffer> buffer;
|
||||
device_->CreateBuffer(desc, &contents, &buffer);
|
||||
return buffer;
|
||||
};
|
||||
const std::byte* next = payload.data();
|
||||
const auto indices = make(next, std::size_t{arrays->indexCount} * 2, BIND_INDEX_BUFFER);
|
||||
next += indexBytes;
|
||||
const auto positions = make(next, vertices * 12, BIND_VERTEX_BUFFER);
|
||||
next += vertices * 12;
|
||||
RefCntAutoPtr<IBuffer> normals, texCoords, colors;
|
||||
if (arrays->hasNormals) {
|
||||
normals = make(next, vertices * 12, BIND_VERTEX_BUFFER);
|
||||
next += vertices * 12;
|
||||
}
|
||||
if (arrays->hasTexCoords) {
|
||||
texCoords = make(next, vertices * 8, BIND_VERTEX_BUFFER);
|
||||
next += vertices * 8;
|
||||
}
|
||||
if (arrays->hasColors) {
|
||||
colors = make(next, vertices * 4, BIND_VERTEX_BUFFER);
|
||||
}
|
||||
draw(indices, arrays->indexCount, positions, normals, texCoords, colors);
|
||||
}
|
||||
|
||||
void Scene::draw(IBuffer* indices, std::uint32_t indexCount, IBuffer* positions, IBuffer* normals, IBuffer* texCoords,
|
||||
IBuffer* colors)
|
||||
{
|
||||
if (indices == nullptr || positions == nullptr || indexCount == 0) {
|
||||
return;
|
||||
}
|
||||
const std::uint32_t streams =
|
||||
(normals != nullptr ? hasNormals : 0) | (texCoords != nullptr ? hasTexCoords : 0) | (colors != nullptr ? hasColors : 0);
|
||||
if (prepare(streams, false) == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
IBuffer* vertexBuffers[4];
|
||||
std::uint32_t count = 0;
|
||||
for (IBuffer* stream : {positions, normals, texCoords, colors}) {
|
||||
if (stream != nullptr) {
|
||||
vertexBuffers[count++] = stream;
|
||||
}
|
||||
}
|
||||
context_->SetVertexBuffers(0, count, vertexBuffers, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION,
|
||||
SET_VERTEX_BUFFERS_FLAG_RESET);
|
||||
context_->SetIndexBuffer(indices, 0, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||
DrawIndexedAttribs attributes;
|
||||
attributes.NumIndices = indexCount;
|
||||
attributes.IndexType = VT_UINT16;
|
||||
context_->DrawIndexed(attributes);
|
||||
}
|
||||
|
||||
Scene::Pipeline* Scene::prepare(std::uint32_t streams, bool sprites)
|
||||
{
|
||||
// Like the client: no textures, the first, or both (the second only with a first).
|
||||
ITextureView* texture0 = textureView(state_.texture0);
|
||||
ITextureView* texture1 = texture0 != nullptr ? textureView(state_.texture1) : nullptr;
|
||||
const std::uint32_t textures = texture0 == nullptr ? 0 : texture1 == nullptr ? 1 : 2;
|
||||
Pipeline* selected = pipeline(streams, textures, sprites);
|
||||
if (selected == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void* mapped = nullptr;
|
||||
context_->MapBuffer(constantBuffer_, MAP_WRITE, MAP_FLAG_DISCARD, mapped);
|
||||
if (mapped == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
std::memcpy(mapped, &constants_, sizeof(constants_));
|
||||
context_->UnmapBuffer(constantBuffer_, MAP_WRITE);
|
||||
|
||||
if (selected->texture0 != nullptr) {
|
||||
selected->texture0->Set(texture0);
|
||||
}
|
||||
if (selected->texture1 != nullptr) {
|
||||
selected->texture1->Set(texture1);
|
||||
}
|
||||
context_->SetPipelineState(selected->state);
|
||||
context_->CommitShaderResources(selected->bindings, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||
return selected;
|
||||
}
|
||||
|
||||
void Scene::drawPointSprites(const protocol::Command& command)
|
||||
{
|
||||
const auto sprites = command.body<protocol::DrawPointSprites>();
|
||||
const auto payload = command.payload<protocol::DrawPointSprites>();
|
||||
if (!sprites || sprites->count == 0 || payload.size() < std::size_t{sprites->count} * 16) {
|
||||
return;
|
||||
}
|
||||
BufferDesc desc;
|
||||
desc.Name = "Client particles";
|
||||
desc.Size = std::size_t{sprites->count} * 16;
|
||||
desc.BindFlags = BIND_VERTEX_BUFFER;
|
||||
desc.Usage = USAGE_IMMUTABLE;
|
||||
BufferData contents;
|
||||
contents.pData = payload.data();
|
||||
contents.DataSize = desc.Size;
|
||||
RefCntAutoPtr<IBuffer> buffer;
|
||||
device_->CreateBuffer(desc, &contents, &buffer);
|
||||
if (!buffer || prepare(0, true) == nullptr) {
|
||||
return;
|
||||
}
|
||||
IBuffer* buffers[] = {buffer};
|
||||
context_->SetVertexBuffers(0, 1, buffers, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION,
|
||||
SET_VERTEX_BUFFERS_FLAG_RESET);
|
||||
DrawAttribs attributes;
|
||||
attributes.NumVertices = 4;
|
||||
attributes.NumInstances = sprites->count;
|
||||
context_->Draw(attributes);
|
||||
}
|
||||
|
||||
const Scene::Shaders& Scene::shaders(std::uint32_t streams, std::uint32_t textures, bool sprites)
|
||||
{
|
||||
const std::uint32_t key = streams | (textures << 3) | (sprites ? 1u << 5 : 0);
|
||||
if (const auto found = shaders_.find(key); found != shaders_.end()) {
|
||||
return found->second;
|
||||
}
|
||||
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_COLORS " + ((streams & hasColors) != 0 ? "1" : "0") +
|
||||
"\n#define TEXTURES " + std::to_string(textures) + "\n" + shaderSource_;
|
||||
Shaders created;
|
||||
ShaderCreateInfo info;
|
||||
info.SourceLanguage = SHADER_SOURCE_LANGUAGE_HLSL;
|
||||
info.Desc.UseCombinedTextureSamplers = true; // g_TextureN with g_TextureN_sampler, set from each texture's view
|
||||
info.Source = source.c_str();
|
||||
info.Desc.ShaderType = SHADER_TYPE_VERTEX;
|
||||
info.Desc.Name = sprites ? "Fixed function particles (vertex)" : "Fixed function (vertex)";
|
||||
info.EntryPoint = sprites ? "vs_sprite" : "vs_main";
|
||||
device_->CreateShader(info, &created.vertex);
|
||||
info.Desc.ShaderType = SHADER_TYPE_PIXEL;
|
||||
info.Desc.Name = "Fixed function (pixel)";
|
||||
info.EntryPoint = "ps_main";
|
||||
device_->CreateShader(info, &created.pixel);
|
||||
return shaders_[key] = std::move(created);
|
||||
}
|
||||
|
||||
Scene::Pipeline* Scene::pipeline(std::uint32_t streams, std::uint32_t textures, bool sprites)
|
||||
{
|
||||
const std::uint32_t blend = state_.blending ? state_.blendMode : 0;
|
||||
const bool culling = state_.culling && !sprites; // point sprites aren't culled
|
||||
const std::uint32_t key = streams | (textures << 3) | (blend << 5) | (state_.depthTest ? 1u << 8 : 0) |
|
||||
(state_.depthWrite ? 1u << 9 : 0) | (culling ? 1u << 10 : 0) |
|
||||
(state_.normalInverting ? 1u << 11 : 0) | (sprites ? 1u << 12 : 0);
|
||||
if (const auto found = pipelines_.find(key); found != pipelines_.end()) {
|
||||
return found->second.state ? &found->second : nullptr;
|
||||
}
|
||||
Pipeline& entry = pipelines_[key];
|
||||
const Shaders& compiled = shaders(streams, textures, sprites);
|
||||
if (!compiled.vertex || !compiled.pixel) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
GraphicsPipelineStateCreateInfo info;
|
||||
info.PSODesc.Name = "Fixed function";
|
||||
info.PSODesc.PipelineType = PIPELINE_TYPE_GRAPHICS;
|
||||
const SwapChainDesc& window = swapChain_->GetDesc();
|
||||
GraphicsPipelineDesc& graphics = info.GraphicsPipeline;
|
||||
graphics.NumRenderTargets = 1;
|
||||
graphics.RTVFormats[0] = window.ColorBufferFormat;
|
||||
graphics.DSVFormat = window.DepthBufferFormat;
|
||||
graphics.PrimitiveTopology = sprites ? PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP : PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
// The client culls clockwise triangles, or counter-clockwise ones when normals are inverted (mirrored objects).
|
||||
graphics.RasterizerDesc.CullMode = culling ? CULL_MODE_BACK : CULL_MODE_NONE;
|
||||
graphics.RasterizerDesc.FrontCounterClockwise = !state_.normalInverting;
|
||||
graphics.DepthStencilDesc.DepthEnable = state_.depthTest;
|
||||
graphics.DepthStencilDesc.DepthWriteEnable = state_.depthWrite;
|
||||
graphics.DepthStencilDesc.DepthFunc = COMPARISON_FUNC_LESS;
|
||||
RenderTargetBlendDesc& blending = graphics.BlendDesc.RenderTargets[0];
|
||||
blending.BlendEnable = blend != 0;
|
||||
if (blend != 0) {
|
||||
const BLEND_FACTOR source = blend == 1 || blend == 4 ? BLEND_FACTOR_SRC_ALPHA : BLEND_FACTOR_ONE;
|
||||
const BLEND_FACTOR destination = blend == 2 || blend == 4 ? BLEND_FACTOR_ONE : BLEND_FACTOR_INV_SRC_ALPHA;
|
||||
blending.SrcBlend = blending.SrcBlendAlpha = source;
|
||||
blending.DestBlend = blending.DestBlendAlpha = destination;
|
||||
blending.BlendOp = blending.BlendOpAlpha = BLEND_OPERATION_ADD;
|
||||
}
|
||||
|
||||
LayoutElement elements[4];
|
||||
std::uint32_t count = 0;
|
||||
if (sprites) {
|
||||
// One buffer of sprites, each a position and a color, read once per square (instance).
|
||||
elements[0] = LayoutElement{0, 0, 3, VT_FLOAT32, False};
|
||||
elements[1] = LayoutElement{3, 0, 4, VT_UINT8, True};
|
||||
elements[1].RelativeOffset = 12;
|
||||
for (LayoutElement& element : std::span(elements).first(2)) {
|
||||
element.Stride = 16;
|
||||
element.Frequency = INPUT_ELEMENT_FREQUENCY_PER_INSTANCE;
|
||||
element.InstanceDataStepRate = 1;
|
||||
}
|
||||
count = 2;
|
||||
} else {
|
||||
elements[count] = LayoutElement{0, count, 3, VT_FLOAT32, False};
|
||||
++count;
|
||||
}
|
||||
if (!sprites && (streams & hasNormals) != 0) {
|
||||
elements[count] = LayoutElement{1, count, 3, VT_FLOAT32, False};
|
||||
++count;
|
||||
}
|
||||
if (!sprites && (streams & hasTexCoords) != 0) {
|
||||
elements[count] = LayoutElement{2, count, 2, VT_FLOAT32, False};
|
||||
++count;
|
||||
}
|
||||
if (!sprites && (streams & hasColors) != 0) {
|
||||
elements[count] = LayoutElement{3, count, 4, VT_UINT8, True};
|
||||
++count;
|
||||
}
|
||||
graphics.InputLayout.LayoutElements = elements;
|
||||
graphics.InputLayout.NumElements = count;
|
||||
|
||||
info.pVS = compiled.vertex;
|
||||
info.pPS = compiled.pixel;
|
||||
const ShaderResourceVariableDesc variables[] = {
|
||||
{SHADER_TYPE_PIXEL, "g_Texture0", SHADER_RESOURCE_VARIABLE_TYPE_DYNAMIC},
|
||||
{SHADER_TYPE_PIXEL, "g_Texture1", SHADER_RESOURCE_VARIABLE_TYPE_DYNAMIC},
|
||||
};
|
||||
info.PSODesc.ResourceLayout.DefaultVariableType = SHADER_RESOURCE_VARIABLE_TYPE_STATIC;
|
||||
info.PSODesc.ResourceLayout.Variables = variables;
|
||||
info.PSODesc.ResourceLayout.NumVariables = textures;
|
||||
device_->CreateGraphicsPipelineState(info, &entry.state);
|
||||
if (!entry.state) {
|
||||
return nullptr;
|
||||
}
|
||||
for (const SHADER_TYPE stage : {SHADER_TYPE_VERTEX, SHADER_TYPE_PIXEL}) {
|
||||
if (IShaderResourceVariable* constants = entry.state->GetStaticVariableByName(stage, "Constants")) {
|
||||
constants->Set(constantBuffer_);
|
||||
}
|
||||
}
|
||||
entry.state->CreateShaderResourceBinding(&entry.bindings, true);
|
||||
if (textures >= 1) {
|
||||
entry.texture0 = entry.bindings->GetVariableByName(SHADER_TYPE_PIXEL, "g_Texture0");
|
||||
}
|
||||
if (textures >= 2) {
|
||||
entry.texture1 = entry.bindings->GetVariableByName(SHADER_TYPE_PIXEL, "g_Texture1");
|
||||
}
|
||||
return &entry;
|
||||
}
|
||||
|
||||
} // namespace t5de
|
||||
@@ -0,0 +1,128 @@
|
||||
#pragma once
|
||||
// Carries out the command stream (include/t5de/protocol/commands.hpp) with Diligent, reproducing IMVU's Direct3D 9
|
||||
// look (shaders/fixed_function.hlsl): the client's textures and buffers by id, its state as it sets it, a pipeline
|
||||
// for each combination of states it draws with.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "EngineFactoryD3D11.h"
|
||||
#include "RefCntAutoPtr.hpp"
|
||||
#include "t5de/protocol/commands.hpp"
|
||||
|
||||
namespace t5de {
|
||||
|
||||
class Scene {
|
||||
public:
|
||||
Scene(Diligent::IRenderDevice* device, Diligent::IDeviceContext* context, Diligent::ISwapChain* swapChain);
|
||||
|
||||
// Carries out one frame's commands. offsetX is where the window starts within the client's 3D view; without draw,
|
||||
// only the frame's changes are made (textures, buffers, state). True if the frame was drawn into the window (to
|
||||
// present).
|
||||
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).
|
||||
[[nodiscard]] std::uint64_t lastSentAt() const { return lastSentAt_; }
|
||||
|
||||
private:
|
||||
struct Texture {
|
||||
Diligent::RefCntAutoPtr<Diligent::ITexture> texture;
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::uint32_t flags = 0;
|
||||
};
|
||||
|
||||
struct Buffer {
|
||||
Diligent::RefCntAutoPtr<Diligent::IBuffer> buffer;
|
||||
protocol::BufferKind kind = protocol::BufferKind::Vertex;
|
||||
std::uint32_t elementSize = 0;
|
||||
std::uint32_t count = 0;
|
||||
};
|
||||
|
||||
struct Pipeline {
|
||||
Diligent::RefCntAutoPtr<Diligent::IPipelineState> state;
|
||||
Diligent::RefCntAutoPtr<Diligent::IShaderResourceBinding> bindings;
|
||||
Diligent::IShaderResourceVariable* texture0 = nullptr;
|
||||
Diligent::IShaderResourceVariable* texture1 = nullptr;
|
||||
};
|
||||
|
||||
struct Shaders {
|
||||
Diligent::RefCntAutoPtr<Diligent::IShader> vertex;
|
||||
Diligent::RefCntAutoPtr<Diligent::IShader> pixel;
|
||||
};
|
||||
|
||||
// Mirrors the shader's constant buffer.
|
||||
struct Constants {
|
||||
float world[16];
|
||||
float view[16];
|
||||
float projection[16];
|
||||
float texTransform0[8];
|
||||
float texTransform1[8];
|
||||
float ambient[4];
|
||||
float lightColor[8][4];
|
||||
float lightVector[8][4];
|
||||
float fogColor[4];
|
||||
float fog[4];
|
||||
float flags[4];
|
||||
float viewport[4];
|
||||
};
|
||||
|
||||
// What a draw needs from the state, as set by the client.
|
||||
struct State {
|
||||
bool depthTest = true;
|
||||
bool depthWrite = true;
|
||||
bool blending = false;
|
||||
std::uint32_t blendMode = 1;
|
||||
bool culling = true;
|
||||
bool normalInverting = false;
|
||||
std::uint32_t texture0 = 0;
|
||||
std::uint32_t texture1 = 0;
|
||||
};
|
||||
|
||||
void createTexture(const protocol::Command& command);
|
||||
void updateTexture(const protocol::Command& command);
|
||||
void createBuffer(const protocol::Command& command);
|
||||
void updateBuffer(const protocol::Command& command);
|
||||
void setProjection(const protocol::SetProjection& projection);
|
||||
void setState(const protocol::SetState& state);
|
||||
void drawTriangles(const protocol::DrawTriangles& draw);
|
||||
void drawArrays(const protocol::Command& command);
|
||||
void drawPointSprites(const protocol::Command& command);
|
||||
|
||||
// Draws indexCount indices from index, with vertex streams (positions required, the others may be null).
|
||||
void draw(Diligent::IBuffer* indices, std::uint32_t indexCount, Diligent::IBuffer* positions,
|
||||
Diligent::IBuffer* normals, Diligent::IBuffer* texCoords, Diligent::IBuffer* colors);
|
||||
// The pipeline for the current state, with these vertex streams and textures, for triangles or particles.
|
||||
Pipeline* pipeline(std::uint32_t streams, std::uint32_t textures, bool sprites);
|
||||
const Shaders& shaders(std::uint32_t streams, std::uint32_t textures, bool sprites);
|
||||
// Picks the pipeline for the current state and sets it up for a draw: constants, textures. Null if it can't.
|
||||
Pipeline* prepare(std::uint32_t streams, bool sprites);
|
||||
Diligent::ITextureView* textureView(std::uint32_t id);
|
||||
|
||||
Diligent::RefCntAutoPtr<Diligent::IRenderDevice> device_;
|
||||
Diligent::RefCntAutoPtr<Diligent::IDeviceContext> context_;
|
||||
Diligent::RefCntAutoPtr<Diligent::ISwapChain> swapChain_;
|
||||
Diligent::RefCntAutoPtr<Diligent::IBuffer> constantBuffer_;
|
||||
Diligent::RefCntAutoPtr<Diligent::ISampler> samplers_[4]; // by wrap u (1) and wrap v (2)
|
||||
|
||||
std::unordered_map<std::uint32_t, Texture> textures_;
|
||||
std::unordered_map<std::uint32_t, Buffer> buffers_;
|
||||
std::unordered_map<std::uint32_t, Shaders> shaders_;
|
||||
std::unordered_map<std::uint32_t, Pipeline> pipelines_;
|
||||
std::string shaderSource_;
|
||||
|
||||
Constants constants_{};
|
||||
State state_;
|
||||
bool onscreen_ = false; // in a frame drawn into the window
|
||||
bool draw_ = true;
|
||||
std::uint64_t lastSentAt_ = 0;
|
||||
int offsetX_ = 0;
|
||||
};
|
||||
|
||||
} // namespace t5de
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
// Generated by CMake from renderer/shaders/@T5DE_SHADER_FILE@: the shader source, compiled at run time by Diligent.
|
||||
|
||||
inline constexpr const char* @T5DE_SHADER_NAME@ = R"hlsl(@T5DE_SHADER_SOURCE@)hlsl";
|
||||
@@ -0,0 +1,146 @@
|
||||
// IMVU's Direct3D 9 fixed-function look, as its renderer sets it up (docs/interface.md):
|
||||
// - per-vertex (Gouraud) lighting, with the vertex color as diffuse and ambient material (white without one): the
|
||||
// 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
|
||||
// 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 by view depth (checked
|
||||
// against the client: fogging by depth buffer value instead looks nothing like it).
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// Compiled for each combination of the vertex streams present (HAS_NORMALS, HAS_TEX_COORDS, HAS_COLORS) and the
|
||||
// number of textures (TEXTURES).
|
||||
|
||||
cbuffer Constants
|
||||
{
|
||||
row_major float4x4 g_World;
|
||||
row_major float4x4 g_View;
|
||||
row_major float4x4 g_Projection;
|
||||
float4 g_TexTransform0[2]; // u' = dot((u, v, 1), [0].xyz), v' = dot((u, v, 1), [1].xyz)
|
||||
float4 g_TexTransform1[2];
|
||||
float4 g_Ambient; // rgb
|
||||
float4 g_LightColor[8]; // rgb, a: enabled
|
||||
float4 g_LightVector[8]; // xyz: position, or direction toward the light; w: 1 if directional
|
||||
float4 g_FogColor; // rgb
|
||||
float4 g_Fog; // x: start, y: end, z: on
|
||||
float4 g_Flags; // x: lighting on, y: alpha test on, z: alpha reference (0-255)
|
||||
float4 g_Viewport; // xy: size in pixels
|
||||
};
|
||||
|
||||
struct VSInput
|
||||
{
|
||||
float3 Position : ATTRIB0;
|
||||
#if HAS_NORMALS
|
||||
float3 Normal : ATTRIB1;
|
||||
#endif
|
||||
#if HAS_TEX_COORDS
|
||||
float2 TexCoord : ATTRIB2;
|
||||
#endif
|
||||
#if HAS_COLORS
|
||||
float4 Color : ATTRIB3; // bytes B, G, R, A
|
||||
#endif
|
||||
};
|
||||
|
||||
struct PSInput
|
||||
{
|
||||
float4 Position : SV_POSITION;
|
||||
float4 Color : COLOR0;
|
||||
float2 TexCoord0 : TEXCOORD0;
|
||||
float2 TexCoord1 : TEXCOORD1;
|
||||
float Depth : TEXCOORD2;
|
||||
};
|
||||
|
||||
void vs_main(in VSInput input, out PSInput output)
|
||||
{
|
||||
float4 world = mul(float4(input.Position, 1.0), g_World);
|
||||
float4 view = mul(world, g_View);
|
||||
output.Position = mul(view, g_Projection);
|
||||
output.Depth = abs(view.z);
|
||||
|
||||
float4 material = float4(1.0, 1.0, 1.0, 1.0);
|
||||
#if HAS_COLORS
|
||||
material = input.Color.zyxw;
|
||||
#endif
|
||||
float4 color = material;
|
||||
if (g_Flags.x > 0.5)
|
||||
{
|
||||
float3 normal = float3(0.0, 0.0, 0.0);
|
||||
#if HAS_NORMALS
|
||||
normal = normalize(mul(input.Normal, (float3x3)g_World));
|
||||
#endif
|
||||
float3 lit = g_Ambient.rgb * material.rgb;
|
||||
for (int i = 0; i < 8; ++i)
|
||||
{
|
||||
if (g_LightColor[i].a > 0.5)
|
||||
{
|
||||
float3 toLight = g_LightVector[i].w > 0.5 ? g_LightVector[i].xyz : g_LightVector[i].xyz - world.xyz;
|
||||
lit += g_LightColor[i].rgb * material.rgb * max(0.0, dot(normal, normalize(toLight)));
|
||||
}
|
||||
}
|
||||
color = float4(saturate(lit), material.a);
|
||||
}
|
||||
output.Color = color;
|
||||
|
||||
float3 uv = float3(0.0, 0.0, 1.0);
|
||||
#if HAS_TEX_COORDS
|
||||
uv.xy = input.TexCoord;
|
||||
#endif
|
||||
output.TexCoord0 = float2(dot(uv, g_TexTransform0[0].xyz), dot(uv, g_TexTransform0[1].xyz));
|
||||
output.TexCoord1 = float2(dot(uv, g_TexTransform1[0].xyz), dot(uv, g_TexTransform1[1].xyz));
|
||||
}
|
||||
|
||||
struct SpriteInput
|
||||
{
|
||||
float3 Position : ATTRIB0;
|
||||
float4 Color : ATTRIB3; // bytes B, G, R, A
|
||||
uint Corner : SV_VertexID;
|
||||
};
|
||||
|
||||
void vs_sprite(in SpriteInput input, out PSInput output)
|
||||
{
|
||||
float4 view = mul(mul(float4(input.Position, 1.0), g_World), g_View);
|
||||
float4 clip = mul(view, g_Projection);
|
||||
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);
|
||||
clip.xy += corner * size / g_Viewport.xy * clip.w; // half the size each way, in clip space
|
||||
output.Position = clip;
|
||||
output.Depth = abs(view.z);
|
||||
|
||||
// No normals: lit by the ambient light alone.
|
||||
float4 material = input.Color.zyxw;
|
||||
output.Color = g_Flags.x > 0.5 ? float4(saturate(g_Ambient.rgb * material.rgb), material.a) : material;
|
||||
output.TexCoord0 = float2(corner.x * 0.5 + 0.5, 0.5 - corner.y * 0.5);
|
||||
output.TexCoord1 = output.TexCoord0;
|
||||
}
|
||||
|
||||
#if TEXTURES >= 1
|
||||
Texture2D g_Texture0;
|
||||
SamplerState g_Texture0_sampler;
|
||||
#endif
|
||||
#if TEXTURES >= 2
|
||||
Texture2D g_Texture1;
|
||||
SamplerState g_Texture1_sampler;
|
||||
#endif
|
||||
|
||||
float4 ps_main(in PSInput input) : SV_Target
|
||||
{
|
||||
float4 color = input.Color;
|
||||
#if TEXTURES == 1
|
||||
color *= g_Texture0.Sample(g_Texture0_sampler, input.TexCoord0);
|
||||
#elif TEXTURES == 2
|
||||
float4 first = g_Texture0.Sample(g_Texture0_sampler, input.TexCoord0);
|
||||
float4 second = g_Texture1.Sample(g_Texture1_sampler, input.TexCoord1);
|
||||
color *= lerp(first, second, second.a);
|
||||
#endif
|
||||
if (g_Flags.y > 0.5 && !(color.a * 255.0 > g_Flags.z))
|
||||
{
|
||||
discard;
|
||||
}
|
||||
if (g_Fog.z > 0.5)
|
||||
{
|
||||
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);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
add_executable(shared_ptr_test shared_ptr_test.cpp)
|
||||
target_link_libraries(shared_ptr_test PRIVATE t5de::imvu t5de::options)
|
||||
add_test(NAME shared_ptr COMMAND shared_ptr_test)
|
||||
|
||||
add_executable(protocol_test protocol_test.cpp)
|
||||
target_link_libraries(protocol_test PRIVATE t5de::protocol t5de::options)
|
||||
add_test(NAME protocol COMMAND protocol_test)
|
||||
@@ -0,0 +1,61 @@
|
||||
// The command stream: what the adapter writes, the renderer process reads back the same.
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "t5de/protocol/commands.hpp"
|
||||
|
||||
using namespace t5de::protocol;
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
|
||||
void check(bool ok, const char* what)
|
||||
{
|
||||
if (!ok) {
|
||||
std::printf("FAIL: %s\n", what);
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
Writer writer;
|
||||
writer.add(Op::Clear, Clear{3, 0xff131313});
|
||||
const unsigned char pixels[] = {1, 2, 3, 4, 5, 6, 7, 8};
|
||||
std::memcpy(writer.add(Op::CreateTexture, CreateTexture{7, 2, 1, 6}, sizeof(pixels)), pixels, sizeof(pixels));
|
||||
std::byte* odd = writer.add(Op::UpdateBuffer, UpdateBuffer{9, 1, 3}, 3); // padded to 4
|
||||
std::memset(odd, 0xab, 3);
|
||||
writer.add(Op::EndFrame, EndFrame{});
|
||||
check(writer.size() % 4 == 0, "commands stay 4-byte aligned");
|
||||
|
||||
Reader reader(writer.bytes());
|
||||
auto clear = reader.next();
|
||||
check(clear && clear->op() == Op::Clear && clear->body<Clear>()->color == 0xff131313, "clear");
|
||||
auto texture = reader.next();
|
||||
check(texture && texture->op() == Op::CreateTexture && texture->body<CreateTexture>()->id == 7, "texture");
|
||||
check(texture && texture->payload<CreateTexture>().size() == sizeof(pixels)
|
||||
&& std::memcmp(texture->payload<CreateTexture>().data(), pixels, sizeof(pixels)) == 0,
|
||||
"texture pixels");
|
||||
auto update = reader.next();
|
||||
check(update && update->payload<UpdateBuffer>().size() == 4, "payload padded");
|
||||
auto end = reader.next();
|
||||
check(end && end->op() == Op::EndFrame, "end of frame");
|
||||
check(!reader.next(), "nothing after");
|
||||
|
||||
// A truncated stream (a chunk cut short) yields only whole commands.
|
||||
Reader truncated(writer.bytes().first(writer.size() - 2));
|
||||
int count = 0;
|
||||
while (truncated.next()) {
|
||||
++count;
|
||||
}
|
||||
check(count == 3, "truncated: whole commands only");
|
||||
|
||||
if (failures == 0) {
|
||||
std::printf("ok\n");
|
||||
}
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in new issue
Block a user