Compare commits
26
Commits
3c28a8794b
..
v0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
869eb19a1f | ||
|
|
4670a6a191 | ||
|
|
d1437a3923 | ||
|
|
2471b1fd4f | ||
|
|
065c7e9300 | ||
|
|
399d934a08 | ||
|
|
05a7be5d41 | ||
|
|
8055099920 | ||
|
|
6e4fe589e5 | ||
|
|
fa883892db | ||
|
|
a021e2caa9 | ||
|
|
7bf29f3e4c | ||
|
|
97b1d36466 | ||
|
|
2d9da249fa | ||
|
|
64e152de3c | ||
|
|
c48c666309 | ||
|
|
7d17cb0b1f | ||
|
|
3cdda4c7ed | ||
|
|
299cd8c312 | ||
|
|
1a45bf9a86 | ||
|
|
a04dba18c1 | ||
|
|
42fc55cb91 | ||
|
|
5c35c8b63a | ||
|
|
9a7d7d2908 | ||
|
|
0d126574bf | ||
|
|
83e0665681 |
No files matched your search
@@ -0,0 +1,153 @@
|
||||
"""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. T5DE's setup installs a pinned release with its T5DE Renderer
|
||||
patch, used when the client's Settings > Advanced > Graphics is OpenGL. To try this build instead, copy both files
|
||||
into a T5DE install's folder (with that 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,7 +1,9 @@
|
||||
# t5de-renderer
|
||||
|
||||
A renderer plugin for IMVU Classic (see README.md and docs/interface.md). Part of T5DE; the patches and the patcher
|
||||
are in the `t5de` repository (git.t5de.dev/t5de/t5de, usually checked out beside this one).
|
||||
A new renderer for IMVU Classic: a 32-bit adapter DLL the client loads as a renderer plugin, and a 64-bit renderer
|
||||
process drawing with Diligent Engine (see README.md, docs/architecture.md, docs/interface.md). Part of T5DE; the
|
||||
patches and the patcher are in the `t5de` repository (git.t5de.dev/t5de/t5de, usually checked out beside this one).
|
||||
The user chose this architecture for ray tracing, mesh shaders and a possible browser client later.
|
||||
|
||||
## Rules
|
||||
|
||||
@@ -13,9 +15,12 @@ are in the `t5de` repository (git.t5de.dev/t5de/t5de, usually checked out beside
|
||||
|
||||
## Notes
|
||||
|
||||
- The plugin must match IMVU's ABI: 32-bit, MSVC (cl or clang-cl; MinGW's C++ ABI differs), `__thiscall`
|
||||
virtual methods in IMVU's vtable order, Boost 1.61's `shared_ptr` layout. Exceptions must not cross into IMVU's
|
||||
code (a different C runtime); the CRT is linked statically.
|
||||
- The adapter must match IMVU's ABI: 32-bit, MSVC (cl or clang-cl; MinGW's C++ ABI differs), `__thiscall`
|
||||
virtual methods in IMVU's vtable order (never overloaded: MSVC orders overloads unlike their declaration), Boost
|
||||
1.61's `shared_ptr` layout. Exceptions must not cross into IMVU's code (a different C runtime); the CRT is linked
|
||||
statically. Code shared with the 64-bit renderer process (`include/t5de/protocol/`) uses fixed-size fields only.
|
||||
- The development VM has no graphics driver (Microsoft Basic Render Driver): Direct3D 11 works in software, IMVU's
|
||||
OpenGL renderer doesn't, and performance there means nothing.
|
||||
- Ghidra 12.2 (Java 25) is at `%LOCALAPPDATA%\Programs\ghidra_12.2_PUBLIC`; projects in
|
||||
`%LOCALAPPDATA%\t5de-dev\ghidra`, analysed headless (`support\analyzeHeadless.bat`). The client to analyse is the
|
||||
`t5de` checkout's `.cache/<version>/install`.
|
||||
+28
-25
@@ -1,19 +1,22 @@
|
||||
cmake_minimum_required(VERSION 3.28)
|
||||
|
||||
project(t5de-renderer
|
||||
VERSION 0.1.0
|
||||
VERSION 0.2.0
|
||||
DESCRIPTION "A new 3D renderer for the IMVU Classic client"
|
||||
LANGUAGES CXX)
|
||||
|
||||
# The plugin is loaded into IMVU Classic, a 32-bit program built with Visual C++: it must use the same ABI.
|
||||
# Two programs, for two architectures (a preset for each, see CMakePresets.json):
|
||||
# - 32-bit: the adapter, t5deRenderEngine.dll, a renderer plugin loaded into IMVU Classic (a 32-bit Visual C++
|
||||
# program, so it must match its ABI).
|
||||
# - 64-bit: the renderer process, t5de-renderer.exe, which draws.
|
||||
if(NOT MSVC) # cl or clang-cl
|
||||
message(FATAL_ERROR "Build with MSVC or clang-cl: the plugin must match IMVU Classic's Visual C++ ABI.")
|
||||
endif()
|
||||
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4)
|
||||
message(FATAL_ERROR "Build for 32-bit x86 (IMVU Classic is 32-bit): use a CMakePresets.json preset.")
|
||||
message(FATAL_ERROR "Build with MSVC or clang-cl: the adapter must match IMVU Classic's Visual C++ ABI.")
|
||||
endif()
|
||||
|
||||
# The C runtime is linked statically, so the plugin needs nothing installed beside it.
|
||||
# No C++ modules: skip scanning for them.
|
||||
set(CMAKE_CXX_SCAN_FOR_MODULES OFF)
|
||||
|
||||
# The C runtime is linked statically, so the programs need nothing installed beside them.
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||
|
||||
# Warnings and conformance for our own targets.
|
||||
@@ -23,25 +26,25 @@ target_compile_features(t5de_options INTERFACE cxx_std_20)
|
||||
target_compile_options(t5de_options INTERFACE /W4 /permissive- /utf-8 /Zc:__cplusplus)
|
||||
target_compile_definitions(t5de_options INTERFACE WIN32_LEAN_AND_MEAN NOMINMAX UNICODE _UNICODE)
|
||||
|
||||
# Our declarations of IMVU's interfaces.
|
||||
add_library(t5de_imvu INTERFACE)
|
||||
add_library(t5de::imvu ALIAS t5de_imvu)
|
||||
target_include_directories(t5de_imvu INTERFACE
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
|
||||
target_compile_features(t5de_imvu INTERFACE cxx_std_20)
|
||||
|
||||
# The renderer plugin, t5deRenderEngine.dll.
|
||||
add_library(t5de_render_engine SHARED
|
||||
src/log.cpp
|
||||
src/passthrough_engine.cpp
|
||||
src/plugin.cpp
|
||||
src/plugin.def)
|
||||
set_target_properties(t5de_render_engine PROPERTIES OUTPUT_NAME t5deRenderEngine)
|
||||
target_link_libraries(t5de_render_engine PRIVATE t5de::imvu t5de::options)
|
||||
|
||||
install(TARGETS t5de_render_engine RUNTIME DESTINATION .)
|
||||
# What the adapter and the renderer process share.
|
||||
add_library(t5de_protocol INTERFACE)
|
||||
add_library(t5de::protocol ALIAS t5de_protocol)
|
||||
target_include_directories(t5de_protocol INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
|
||||
target_compile_features(t5de_protocol INTERFACE cxx_std_20)
|
||||
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 4)
|
||||
# Our declarations of IMVU's interfaces (32-bit layouts).
|
||||
add_library(t5de_imvu INTERFACE)
|
||||
add_library(t5de::imvu ALIAS t5de_imvu)
|
||||
target_include_directories(t5de_imvu INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
|
||||
target_compile_features(t5de_imvu INTERFACE cxx_std_20)
|
||||
|
||||
add_subdirectory(adapter)
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
else()
|
||||
add_subdirectory(renderer)
|
||||
endif()
|
||||
+33
-12
@@ -3,29 +3,43 @@
|
||||
"cmakeMinimumRequired": { "major": 3, "minor": 28, "patch": 0 },
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "windows-x86",
|
||||
"displayName": "Windows, 32-bit (Visual Studio Build Tools)",
|
||||
"description": "IMVU Classic is a 32-bit Visual C++ program: build for Win32 with MSVC.",
|
||||
"name": "windows",
|
||||
"hidden": true,
|
||||
"generator": "Visual Studio 18 2026",
|
||||
"architecture": { "value": "Win32", "strategy": "set" },
|
||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||
"installDir": "${sourceDir}/build/${presetName}/install",
|
||||
"installDir": "${sourceDir}/build/install",
|
||||
"condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }
|
||||
},
|
||||
{
|
||||
"name": "windows-x86",
|
||||
"inherits": "windows",
|
||||
"displayName": "Adapter (32-bit)",
|
||||
"description": "t5deRenderEngine.dll, loaded into IMVU Classic, a 32-bit Visual C++ program.",
|
||||
"architecture": { "value": "Win32", "strategy": "set" }
|
||||
},
|
||||
{
|
||||
"name": "windows-x64",
|
||||
"inherits": "windows",
|
||||
"displayName": "Renderer process (64-bit)",
|
||||
"description": "t5de-renderer.exe, with Diligent Engine (fetched and built on the first configure).",
|
||||
"architecture": { "value": "x64", "strategy": "set" }
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{ "name": "debug", "configurePreset": "windows-x86", "configuration": "Debug" },
|
||||
{ "name": "release", "configurePreset": "windows-x86", "configuration": "Release" }
|
||||
{ "name": "adapter-debug", "configurePreset": "windows-x86", "configuration": "Debug" },
|
||||
{ "name": "adapter-release", "configurePreset": "windows-x86", "configuration": "Release" },
|
||||
{ "name": "renderer-debug", "configurePreset": "windows-x64", "configuration": "Debug" },
|
||||
{ "name": "renderer-release", "configurePreset": "windows-x64", "configuration": "Release" }
|
||||
],
|
||||
"testPresets": [
|
||||
{
|
||||
"name": "debug",
|
||||
"name": "adapter-debug",
|
||||
"configurePreset": "windows-x86",
|
||||
"configuration": "Debug",
|
||||
"output": { "outputOnFailure": true }
|
||||
},
|
||||
{
|
||||
"name": "release",
|
||||
"name": "adapter-release",
|
||||
"configurePreset": "windows-x86",
|
||||
"configuration": "Release",
|
||||
"output": { "outputOnFailure": true }
|
||||
@@ -33,11 +47,18 @@
|
||||
],
|
||||
"workflowPresets": [
|
||||
{
|
||||
"name": "release",
|
||||
"name": "adapter",
|
||||
"steps": [
|
||||
{ "type": "configure", "name": "windows-x86" },
|
||||
{ "type": "build", "name": "release" },
|
||||
{ "type": "test", "name": "release" }
|
||||
{ "type": "build", "name": "adapter-release" },
|
||||
{ "type": "test", "name": "adapter-release" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "renderer",
|
||||
"steps": [
|
||||
{ "type": "configure", "name": "windows-x64" },
|
||||
{ "type": "build", "name": "renderer-release" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
# t5de-renderer
|
||||
|
||||
A new 3D renderer for the IMVU Classic client, as a renderer plugin the client loads like its own OpenGL renderer.
|
||||
Part of [T5DE](https://git.t5de.dev/t5de/t5de); unofficial, not affiliated with IMVU.
|
||||
A new 3D renderer for the IMVU Classic client. Part of [T5DE](https://git.t5de.dev/t5de/t5de); unofficial, not
|
||||
affiliated with IMVU.
|
||||
|
||||
For now it's a pass-through: it wraps IMVU's Direct3D renderer and logs what each frame draws, to prove our
|
||||
declarations of IMVU's interface (`docs/interface.md`).
|
||||
Two programs (`docs/architecture.md`): an adapter, `t5deRenderEngine.dll`, which IMVU Classic loads as a renderer
|
||||
plugin, and a 64-bit renderer process, `t5de-renderer.exe`, which draws with
|
||||
[Diligent Engine](https://github.com/DiligentGraphics/DiligentCore). For now the client's own Direct3D renderer still
|
||||
draws the scene, and the renderer process proves the setup by drawing a small box over it. `docs/interface.md` has
|
||||
what we know of IMVU's renderer interface.
|
||||
|
||||
## Building
|
||||
|
||||
Needs Visual Studio Build Tools 2026 (the C++ workload) and CMake 3.28 or newer. IMVU Classic is a 32-bit
|
||||
Visual C++ program, so the plugin is built for 32-bit Windows (the `windows-x86` preset):
|
||||
Needs Visual Studio Build Tools 2026 (the C++ workload) and CMake 3.28 or newer. One preset per program:
|
||||
|
||||
```sh
|
||||
cmake --workflow --preset release
|
||||
cmake --workflow --preset adapter # build/windows-x86/adapter/Release/t5deRenderEngine.dll, and the tests
|
||||
cmake --workflow --preset renderer # build/windows-x64/renderer/Release/t5de-renderer.exe
|
||||
```
|
||||
|
||||
That configures, builds `build/windows-x86/Release/t5deRenderEngine.dll` and runs the tests. Or step by step:
|
||||
`cmake --preset windows-x86`, `cmake --build --preset debug`, `ctest --preset debug`.
|
||||
The renderer's first configure fetches Diligent Engine (pinned in `renderer/CMakeLists.txt`) and builds it, which
|
||||
takes a while.
|
||||
|
||||
## Trying it
|
||||
|
||||
Copy `t5deRenderEngine.dll` into a T5DE client folder built with the `t5de-renderer` patch, which has the OpenGL
|
||||
setting load it in place of `glRenderEngine.dll`, then choose OpenGL in Settings > Advanced. The log is
|
||||
Copy both programs into a T5DE client folder built with the `t5de-renderer` patch, which has the OpenGL setting load
|
||||
the adapter in place of `glRenderEngine.dll`, then choose OpenGL in Settings > Advanced. The adapter's log is
|
||||
`%TEMP%\t5de-renderer.log`.
|
||||
@@ -0,0 +1,17 @@
|
||||
# t5deRenderEngine.dll, the adapter: a renderer plugin for IMVU Classic (32-bit).
|
||||
|
||||
add_library(t5de_render_engine SHARED
|
||||
engine.cpp
|
||||
frame_pacer.cpp
|
||||
log.cpp
|
||||
pixmap.cpp
|
||||
plugin.cpp
|
||||
plugin.def
|
||||
recorder.cpp
|
||||
renderer_process.cpp
|
||||
resources.cpp
|
||||
scene_hooks.cpp)
|
||||
set_target_properties(t5de_render_engine PROPERTIES OUTPUT_NAME t5deRenderEngine)
|
||||
target_link_libraries(t5de_render_engine PRIVATE t5de::imvu t5de::protocol t5de::options)
|
||||
|
||||
install(TARGETS t5de_render_engine RUNTIME DESTINATION .)
|
||||
@@ -0,0 +1,448 @@
|
||||
#include "engine.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "frame_pacer.hpp"
|
||||
#include "inner_timing.hpp"
|
||||
#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, HWND window)
|
||||
: inner_(std::move(inner)), recorder_(std::move(recorder)), window_(window), innerTicksAtStats_(innerTicks)
|
||||
{
|
||||
QueryPerformanceCounter(&statsStart_);
|
||||
if (window_ != nullptr) {
|
||||
frame_pacer::add(window_);
|
||||
}
|
||||
}
|
||||
|
||||
Engine::~Engine()
|
||||
{
|
||||
if (window_ != nullptr) {
|
||||
frame_pacer::remove(window_);
|
||||
}
|
||||
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) { const InnerCall timed; inner_->resizeFrameBuffer(size); }
|
||||
|
||||
void Engine::enable2dFrameBuffer() { const InnerCall timed; inner_->enable2dFrameBuffer(); }
|
||||
|
||||
void Engine::clear(std::uint32_t flags, imvu::Color color)
|
||||
{
|
||||
if (innerDraws()) {
|
||||
{ const InnerCall timed; inner_->clear(flags, color); }
|
||||
}
|
||||
recorder_->add(Op::Clear, protocol::Clear{flags, color});
|
||||
}
|
||||
|
||||
void Engine::setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection)
|
||||
{
|
||||
{ const InnerCall timed; 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)
|
||||
{
|
||||
{ const InnerCall timed; inner_->setModelSpace(model); }
|
||||
recorder_->add(Op::SetModel, transform_command(model));
|
||||
}
|
||||
|
||||
void Engine::setViewSpace(const imvu::Transform& view)
|
||||
{
|
||||
{ const InnerCall timed; inner_->setViewSpace(view); }
|
||||
recorder_->add(Op::SetView, transform_command(view));
|
||||
}
|
||||
|
||||
void Engine::setTextures(imvu::PiRenderTexture* texture0, imvu::PiRenderTexture* texture1)
|
||||
{
|
||||
{ const InnerCall timed; 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)
|
||||
{
|
||||
{ const InnerCall timed; inner_->setDepthTestingOn(on); }
|
||||
setState(State::DepthTest, on);
|
||||
}
|
||||
|
||||
void Engine::setDepthWritingOn(bool on)
|
||||
{
|
||||
{ const InnerCall timed; inner_->setDepthWritingOn(on); }
|
||||
setState(State::DepthWrite, on);
|
||||
}
|
||||
|
||||
void Engine::setBlendMode(imvu::RenderBlendMode mode)
|
||||
{
|
||||
{ const InnerCall timed; inner_->setBlendMode(mode); }
|
||||
setState(State::BlendMode, static_cast<std::uint32_t>(mode));
|
||||
}
|
||||
|
||||
void Engine::setAlphaTestingOn(bool on)
|
||||
{
|
||||
{ const InnerCall timed; inner_->setAlphaTestingOn(on); }
|
||||
setState(State::AlphaTest, on);
|
||||
}
|
||||
|
||||
void Engine::setAlphaTestValue(float value)
|
||||
{
|
||||
{ const InnerCall timed; inner_->setAlphaTestValue(value); }
|
||||
setState(State::AlphaReference, static_cast<std::uint32_t>(std::lround(value * 255.0f)));
|
||||
}
|
||||
|
||||
void Engine::setBlendingOn(bool on)
|
||||
{
|
||||
{ const InnerCall timed; inner_->setBlendingOn(on); }
|
||||
setState(State::Blending, on);
|
||||
}
|
||||
|
||||
void Engine::setBackFaceCullingOn(bool on)
|
||||
{
|
||||
{ const InnerCall timed; inner_->setBackFaceCullingOn(on); }
|
||||
setState(State::Culling, on);
|
||||
}
|
||||
|
||||
void Engine::setNormalInvertingOn(bool on)
|
||||
{
|
||||
{ const InnerCall timed; inner_->setNormalInvertingOn(on); }
|
||||
setState(State::NormalInverting, on);
|
||||
}
|
||||
|
||||
void Engine::setLightingOn(bool on)
|
||||
{
|
||||
{ const InnerCall timed; inner_->setLightingOn(on); }
|
||||
setState(State::Lighting, on);
|
||||
}
|
||||
|
||||
void Engine::setLightProperties(std::uint32_t index, const imvu::LightProperties& light)
|
||||
{
|
||||
{ const InnerCall timed; 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)
|
||||
{
|
||||
{ const InnerCall timed; inner_->setFogOn(on); }
|
||||
setState(State::Fog, on);
|
||||
}
|
||||
|
||||
void Engine::setFogAttributes(float start, float end, imvu::Color color)
|
||||
{
|
||||
{ const InnerCall timed; inner_->setFogAttributes(start, end, color); }
|
||||
recorder_->add(Op::SetFog, protocol::SetFog{start, end, color});
|
||||
}
|
||||
|
||||
void Engine::setTextureTransformMatrixIdentity(std::uint32_t stage)
|
||||
{
|
||||
{ const InnerCall timed; 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)
|
||||
{
|
||||
{ const InnerCall timed; 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)
|
||||
{
|
||||
{ const InnerCall timed; 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);
|
||||
if (!recorder_->active() || size.x <= 0 || size.y <= 0) {
|
||||
return inner_->newPixmapRenderTarget(size);
|
||||
}
|
||||
return imvu::SharedPtr<PixmapTarget>(std::make_unique<PixmapTarget>(recorder_, size));
|
||||
}
|
||||
|
||||
imvu::SharedPtr<imvu::PiTextureRenderTarget> Engine::newTextureRenderTarget(imvu::Vec2i size)
|
||||
{
|
||||
log::info("engine: frame {}: a {}x{} texture render target", frames_, size.x, size.y);
|
||||
if (!recorder_->active() || size.x <= 0 || size.y <= 0) {
|
||||
return inner_->newTextureRenderTarget(size);
|
||||
}
|
||||
return imvu::SharedPtr<TextureTarget>(std::make_unique<TextureTarget>(recorder_, size));
|
||||
}
|
||||
|
||||
void Engine::beginRendering(imvu::PiRenderTarget* target)
|
||||
{
|
||||
QueryPerformanceCounter(&frameStart_);
|
||||
windowFrame_ = target == nullptr;
|
||||
if (windowFrame_) {
|
||||
if (lastWindowFrame_ != 0) {
|
||||
LARGE_INTEGER frequency;
|
||||
QueryPerformanceFrequency(&frequency);
|
||||
longestGap_ = std::max(longestGap_, static_cast<double>(frameStart_.QuadPart - lastWindowFrame_) /
|
||||
static_cast<double>(frequency.QuadPart));
|
||||
}
|
||||
lastWindowFrame_ = frameStart_.QuadPart;
|
||||
}
|
||||
// Frames into our render targets are drawn by the renderer process only; the client's renderer doesn't know them.
|
||||
ourTarget_ = ours(target) ? target : nullptr;
|
||||
// Decided per frame: frames into the client renderer's render targets, and frames while the renderer process
|
||||
// doesn't cover the view, are drawn by the client's renderer.
|
||||
const bool takenOver = ourTarget_ != nullptr || (target == nullptr && recorder_->covering());
|
||||
// Frames the client's renderer doesn't draw aren't begun in it either: its endRendering presents, which can wait
|
||||
// for the display (and would show nothing: the renderer process's window covers it).
|
||||
innerFrame_ = !takenOver;
|
||||
if (innerFrame_) {
|
||||
{ const InnerCall timed; inner_->beginRendering(target); }
|
||||
}
|
||||
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 {} render target ({})", frames_,
|
||||
ourTarget_ != nullptr ? "our" : "the client renderer's", static_cast<void*>(target));
|
||||
}
|
||||
protocol::BeginFrame begin{target == nullptr ? 0 : protocol::foreignTarget, 0};
|
||||
if (const auto* pixmap = dynamic_cast<const PixmapTarget*>(ourTarget_)) {
|
||||
begin = {pixmap->id(), 1};
|
||||
} else if (const auto* texture = dynamic_cast<const TextureTarget*>(ourTarget_)) {
|
||||
begin = {texture->id(), 0};
|
||||
}
|
||||
recorder_->add(Op::BeginFrame, begin);
|
||||
}
|
||||
|
||||
void Engine::endRendering()
|
||||
{
|
||||
if (innerFrame_) {
|
||||
{ const InnerCall timed; inner_->endRendering(); }
|
||||
}
|
||||
LARGE_INTEGER now;
|
||||
QueryPerformanceCounter(&now);
|
||||
recorder_->add(Op::EndFrame, protocol::EndFrame{static_cast<std::uint64_t>(now.QuadPart)});
|
||||
recorder_->flush(true);
|
||||
if (auto* pixmap = dynamic_cast<PixmapTarget*>(ourTarget_)) {
|
||||
pixmap->setPixels(recorder_->readBack());
|
||||
} else if (windowFrame_ && window_ != nullptr) {
|
||||
LARGE_INTEGER end, frequency;
|
||||
QueryPerformanceCounter(&end);
|
||||
QueryPerformanceFrequency(&frequency);
|
||||
const double seconds =
|
||||
static_cast<double>(end.QuadPart - frameStart_.QuadPart) / static_cast<double>(frequency.QuadPart);
|
||||
frameSeconds_ += seconds;
|
||||
frame_pacer::frameDrawn(window_, seconds);
|
||||
}
|
||||
ourTarget_ = nullptr;
|
||||
++frames_;
|
||||
if (frames_ % 300 == 0) {
|
||||
LARGE_INTEGER ended, frequency;
|
||||
QueryPerformanceCounter(&ended);
|
||||
QueryPerformanceFrequency(&frequency);
|
||||
const double seconds =
|
||||
static_cast<double>(ended.QuadPart - statsStart_.QuadPart) / static_cast<double>(frequency.QuadPart);
|
||||
statsStart_ = ended;
|
||||
const Recorder::Counters sent = recorder_->takeCounters();
|
||||
if (statsLogged_ < 6 || frames_ % 3000 == 0) {
|
||||
++statsLogged_;
|
||||
log::info("engine: last 300 frames: {:.0f} per second, at most {:.1f} ms apart; each {:.2f} ms beginRendering to "
|
||||
"endRendering, {:.2f} ms in the client renderer's methods, {:.2f} ms sending, {:.0f} KB sent, {:.1f} "
|
||||
"buffer updates ({:.0f} KB); drawTriangles {} (and {} with buffers not ours), drawTrianglesFromMemory "
|
||||
"{} (and {} without indices), point sprites {}, lines {}, 2D projections {}",
|
||||
300 / seconds, longestGap_ * 1000, frameSeconds_ * 1000 / 300,
|
||||
static_cast<double>(innerTicks - innerTicksAtStats_) * 1000 / static_cast<double>(frequency.QuadPart) / 300,
|
||||
sent.sendSeconds * 1000 / 300,
|
||||
static_cast<double>(sent.bytes) / 1024 / 300, sent.bufferUpdates / 300.0,
|
||||
static_cast<double>(sent.bufferBytes) / 1024 / 300, stats_.triangles, stats_.foreignTriangles,
|
||||
stats_.indexed, stats_.unindexed, stats_.sprites, stats_.lines, stats_.flat);
|
||||
}
|
||||
stats_ = {};
|
||||
frameSeconds_ = 0;
|
||||
longestGap_ = 0;
|
||||
innerTicksAtStats_ = innerTicks;
|
||||
}
|
||||
}
|
||||
|
||||
void Engine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
|
||||
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
|
||||
imvu::PiInternalVertexBuffer* colors)
|
||||
{
|
||||
if (innerDraws()) {
|
||||
{ const InnerCall timed; 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()) {
|
||||
{ const InnerCall timed; inner_->drawLine(from, to, color); }
|
||||
}
|
||||
++stats_.lines;
|
||||
}
|
||||
|
||||
void Engine::drawPointSprites(const imvu::PointSpriteVector& sprites)
|
||||
{
|
||||
if (innerDraws()) {
|
||||
{ const InnerCall timed; 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()) {
|
||||
{ const InnerCall timed; 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
|
||||
@@ -0,0 +1,104 @@
|
||||
#pragma once
|
||||
// The adapter's engine: what the client draws with. Everything is recorded for the renderer process, which draws the
|
||||
// 3D view and the render targets it makes. The client's own Direct3D renderer gets state and resources too (as a
|
||||
// fallback, and for reading textures back), 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 <windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "recorder.hpp"
|
||||
#include "t5de/imvu/render_engine.hpp"
|
||||
|
||||
namespace t5de {
|
||||
|
||||
class Engine final : public imvu::PiRenderEngine {
|
||||
public:
|
||||
// recorder may be inactive (no renderer process); window is the 3D window drawn into (frame_pacer.hpp), or null.
|
||||
Engine(imvu::SharedPtr<imvu::PiRenderEngine> inner, std::shared_ptr<Recorder> recorder, HWND window);
|
||||
~Engine() override;
|
||||
|
||||
void resizeFrameBuffer(imvu::Vec2i size) override;
|
||||
void enable2dFrameBuffer() override;
|
||||
void clear(std::uint32_t flags, imvu::Color color) override;
|
||||
void setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection) override;
|
||||
void setModelSpace(const imvu::Transform& model) override;
|
||||
void setViewSpace(const imvu::Transform& view) override;
|
||||
void setTextures(imvu::PiRenderTexture* texture0, imvu::PiRenderTexture* texture1) override;
|
||||
void setDepthTestingOn(bool on) override;
|
||||
void setDepthWritingOn(bool on) override;
|
||||
void setBlendMode(imvu::RenderBlendMode mode) override;
|
||||
void setAlphaTestingOn(bool on) override;
|
||||
void setAlphaTestValue(float value) override;
|
||||
void setBlendingOn(bool on) override;
|
||||
void setBackFaceCullingOn(bool on) override;
|
||||
void setNormalInvertingOn(bool on) override;
|
||||
void setLightingOn(bool on) override;
|
||||
void setLightProperties(std::uint32_t index, const imvu::LightProperties& light) override;
|
||||
void setFogOn(bool on) override;
|
||||
void setFogAttributes(float start, float end, imvu::Color color) override;
|
||||
void setTextureTransformMatrixIdentity(std::uint32_t stage) override;
|
||||
void setTextureTransformMatrix(std::uint32_t stage, const imvu::TextureTransform& transform) override;
|
||||
void setEnvironmentAmbient(imvu::Color color) override;
|
||||
imvu::PiRenderTexture* newRenderTexture(const imvu::Pixmap& pixmap, std::uint32_t flags) override;
|
||||
imvu::PiInternalIndexBuffer* newIndexBuffer(std::uint32_t count, const std::uint16_t* indices) override;
|
||||
imvu::PiInternalVertexBuffer* newStaticVertexBuffer(const void* data, std::uint32_t stride,
|
||||
std::uint32_t elementSize,
|
||||
std::uint32_t elementCount) override;
|
||||
imvu::PiInternalVertexBuffer* newDynamicVertexBuffer(std::uint32_t elementSize,
|
||||
std::uint32_t elementCount) override;
|
||||
imvu::SharedPtr<imvu::PiPixmapRenderTarget> newPixmapRenderTarget(imvu::Vec2i size) override;
|
||||
imvu::SharedPtr<imvu::PiTextureRenderTarget> newTextureRenderTarget(imvu::Vec2i size) override;
|
||||
void beginRendering(imvu::PiRenderTarget* target) override;
|
||||
void endRendering() override;
|
||||
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, 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,
|
||||
const imvu::VertexAttributeArray<imvu::Vec3f>& normals,
|
||||
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
|
||||
const imvu::VertexAttributeArray<imvu::Color>& colors) override;
|
||||
|
||||
private:
|
||||
void setState(protocol::State state, std::uint32_t value);
|
||||
// Whether the client's renderer should draw this frame's clears and draws.
|
||||
[[nodiscard]] bool innerDraws() const { return !takenOver_; }
|
||||
|
||||
imvu::SharedPtr<imvu::PiRenderEngine> inner_;
|
||||
std::shared_ptr<Recorder> recorder_;
|
||||
HWND window_;
|
||||
std::uint64_t frames_ = 0;
|
||||
bool takenOver_ = false; // this frame is drawn by the renderer process only
|
||||
imvu::PiRenderTarget* ourTarget_ = nullptr; // this frame's render target, if it's one of ours
|
||||
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_;
|
||||
LARGE_INTEGER statsStart_{}; // when the stats' 300 frames started
|
||||
LARGE_INTEGER frameStart_{}; // when the current frame began
|
||||
std::int64_t lastWindowFrame_ = 0; // when the last window frame began
|
||||
double longestGap_ = 0; // the stats' longest time between window frames' beginnings, in seconds
|
||||
double frameSeconds_ = 0; // the stats' window frames' time, beginRendering to endRendering
|
||||
bool windowFrame_ = false; // the current frame is drawn into the window
|
||||
bool innerFrame_ = false; // the current frame is begun in the client's renderer
|
||||
std::int64_t innerTicksAtStats_ = 0; // innerTicks when the stats' 300 frames started
|
||||
int statsLogged_ = 0;
|
||||
|
||||
};
|
||||
|
||||
} // namespace t5de
|
||||
@@ -0,0 +1,167 @@
|
||||
#include "frame_pacer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "log.hpp"
|
||||
#include "scene_hooks.hpp"
|
||||
|
||||
namespace t5de::frame_pacer {
|
||||
|
||||
namespace {
|
||||
|
||||
// The client's own rate: its ticks already draw these frames.
|
||||
constexpr DWORD clientRate = 30;
|
||||
// The client is left idle at least this share of a frame's time between frames.
|
||||
constexpr double idleShare = 1.0 / 3.0;
|
||||
// The client's tick (SceneWindow.dll's millisecondsPerFrame).
|
||||
constexpr double tickSeconds = 0.033;
|
||||
// An extra frame must end at least this long before the next tick is due.
|
||||
constexpr double tickMargin = 0.001;
|
||||
|
||||
// The user's frame rate, from the registry (unset or unreadable: the client's own).
|
||||
DWORD read_rate()
|
||||
{
|
||||
DWORD rate = 0;
|
||||
DWORD size = sizeof(rate);
|
||||
if (RegGetValueW(HKEY_CURRENT_USER, L"Software\\T5DE\\Graphics", L"FrameRate", RRF_RT_REG_DWORD, nullptr, &rate,
|
||||
&size) != ERROR_SUCCESS) {
|
||||
return clientRate;
|
||||
}
|
||||
return rate;
|
||||
}
|
||||
|
||||
double seconds(std::int64_t counter)
|
||||
{
|
||||
static const double frequency = [] {
|
||||
LARGE_INTEGER value;
|
||||
QueryPerformanceFrequency(&value);
|
||||
return static_cast<double>(value.QuadPart);
|
||||
}();
|
||||
return static_cast<double>(counter) / frequency;
|
||||
}
|
||||
|
||||
double now_seconds()
|
||||
{
|
||||
LARGE_INTEGER counter;
|
||||
QueryPerformanceCounter(&counter);
|
||||
return seconds(counter.QuadPart);
|
||||
}
|
||||
|
||||
class Pacer {
|
||||
public:
|
||||
static Pacer& get()
|
||||
{
|
||||
static Pacer pacer;
|
||||
return pacer;
|
||||
}
|
||||
|
||||
void add(HWND window)
|
||||
{
|
||||
const std::scoped_lock lock(mutex_);
|
||||
windows_.push_back({.window = window});
|
||||
if (!started_) {
|
||||
started_ = true;
|
||||
std::thread([this] { run(); }).detach(); // runs while the client does
|
||||
}
|
||||
}
|
||||
|
||||
void remove(HWND window)
|
||||
{
|
||||
const std::scoped_lock lock(mutex_);
|
||||
std::erase_if(windows_, [window](const Window& w) { return w.window == window; });
|
||||
}
|
||||
|
||||
void frameDrawn(HWND window, double seconds)
|
||||
{
|
||||
const std::scoped_lock lock(mutex_);
|
||||
for (Window& w : windows_) {
|
||||
if (w.window == window) {
|
||||
w.pending = false;
|
||||
w.frameTime = w.frameTime == 0 ? seconds : w.frameTime * 0.9 + seconds * 0.1;
|
||||
w.drawnAt = now_seconds();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
struct Window {
|
||||
HWND window;
|
||||
bool pending = false; // a repaint asked for and not yet drawn
|
||||
double frameTime = 0; // seconds a frame takes the client, averaged
|
||||
double drawnAt = 0; // when its last frame was drawn
|
||||
double askedAt = 0; // when a repaint was last asked for
|
||||
};
|
||||
|
||||
// Re-reads the setting now and then; above the client's rate, asks each window for repaints at the rate, or as often
|
||||
// as leaves the client its idle time.
|
||||
void run()
|
||||
{
|
||||
HANDLE timer = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
|
||||
double checked = 0;
|
||||
DWORD rate = clientRate;
|
||||
for (;;) {
|
||||
const double now = now_seconds();
|
||||
if (now - checked >= 0.5) {
|
||||
checked = now;
|
||||
const DWORD read = read_rate();
|
||||
if (read != rate) {
|
||||
rate = read;
|
||||
log::info("frame pacer: frame rate {}", rate == 0 ? std::string("uncapped") : std::to_string(rate));
|
||||
}
|
||||
}
|
||||
if (rate != 0 && rate <= clientRate) {
|
||||
Sleep(100);
|
||||
continue;
|
||||
}
|
||||
const double interval = rate == 0 ? 0 : 1.0 / rate;
|
||||
// The next tick, if known (scene_hooks.hpp): extra frames mustn't hold it up, or the client catches up two
|
||||
// steps at once (visible hitches).
|
||||
const std::int64_t lastStep = scene_hooks::lastStep();
|
||||
const double untilTick = lastStep != 0 ? seconds(lastStep) + tickSeconds - now : tickSeconds;
|
||||
{
|
||||
const std::scoped_lock lock(mutex_);
|
||||
for (Window& w : windows_) {
|
||||
if (w.pending && now - w.askedAt < 0.25) {
|
||||
continue; // not drawn yet (asked again after a while: the client may have skipped it)
|
||||
}
|
||||
if (untilTick > -tickSeconds && untilTick < w.frameTime + tickMargin) {
|
||||
continue; // the tick is due: it draws a frame of its own
|
||||
}
|
||||
// Since the last frame ended: the rest of the interval since it began, and the idle time.
|
||||
if (now - w.drawnAt >= std::max(interval - w.frameTime, w.frameTime * idleShare)) {
|
||||
w.pending = true;
|
||||
w.askedAt = now;
|
||||
InvalidateRect(w.window, nullptr, FALSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Wake often enough for the rate (1 ms at most when uncapped).
|
||||
const double wait = std::clamp(interval / 4, 0.0005, 0.004);
|
||||
LARGE_INTEGER due{.QuadPart = -static_cast<LONGLONG>(wait * 10'000'000)}; // relative, 100 ns units
|
||||
if (timer != nullptr && SetWaitableTimer(timer, &due, 0, nullptr, nullptr, FALSE)) {
|
||||
WaitForSingleObject(timer, INFINITE);
|
||||
} else {
|
||||
Sleep(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::mutex mutex_;
|
||||
std::vector<Window> windows_;
|
||||
bool started_ = false;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void add(HWND window) { Pacer::get().add(window); }
|
||||
|
||||
void remove(HWND window) { Pacer::get().remove(window); }
|
||||
|
||||
void frameDrawn(HWND window, double seconds) { Pacer::get().frameDrawn(window, seconds); }
|
||||
|
||||
} // namespace t5de::frame_pacer
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
// Extra frames between the client's 30-per-second ticks. The client draws a 3D window's frame when the window gets
|
||||
// WM_PAINT, and steps its scene on its own timer (SceneWindow.dll's runApp: a step, then the window is invalidated);
|
||||
// its message loop also wakes for WM_PAINT between steps. So asking the windows to repaint more often makes the client
|
||||
// draw more frames, without stepping the scene. The rate is the user's (T5DE's settings, Graphics: HKCU\Software\T5DE\
|
||||
// Graphics, FrameRate): 30, the client's own and the default, adds nothing; 60, 120, 240 or 360 add frames between
|
||||
// ticks; 0 is uncapped.
|
||||
//
|
||||
// The rate counts all of a window's frames, the client's own included: a window is asked for one once its last frame
|
||||
// is drawn and the interval since it began has passed. Drawing runs on the client's main thread, with its input and
|
||||
// interface, so the client is also always left idle a third of a frame's time between frames: when the rate can't be
|
||||
// reached, the client draws as many frames as that allows.
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace t5de::frame_pacer {
|
||||
|
||||
// A 3D window an engine draws into: repainted at the user's frame rate while added.
|
||||
void add(HWND window);
|
||||
void remove(HWND window);
|
||||
|
||||
// A frame drawn into window, taking the client this long (beginRendering to endRendering).
|
||||
void frameDrawn(HWND window, double seconds);
|
||||
|
||||
} // namespace t5de::frame_pacer
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
// Diagnostics: the time spent in the client renderer's methods (the engine's inner engine, and the resources it made),
|
||||
// summed, so the stats can tell it from the rest of a frame.
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace t5de {
|
||||
|
||||
// QueryPerformanceCounter ticks spent in the client renderer's methods so far.
|
||||
inline std::int64_t innerTicks = 0;
|
||||
|
||||
// Adds its lifetime to innerTicks.
|
||||
class InnerCall {
|
||||
public:
|
||||
InnerCall() { QueryPerformanceCounter(&start_); }
|
||||
InnerCall(const InnerCall&) = delete;
|
||||
InnerCall& operator=(const InnerCall&) = delete;
|
||||
~InnerCall()
|
||||
{
|
||||
LARGE_INTEGER end;
|
||||
QueryPerformanceCounter(&end);
|
||||
innerTicks += end.QuadPart - start_.QuadPart;
|
||||
}
|
||||
|
||||
private:
|
||||
LARGE_INTEGER start_;
|
||||
};
|
||||
|
||||
} // namespace t5de
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,83 @@
|
||||
#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*);
|
||||
using SizeConstructor = imvu::Pixmap*(__thiscall*)(void*, int, int);
|
||||
using DefaultConstructor = imvu::Pixmap*(__thiscall*)(void*);
|
||||
using CopyConstructor = imvu::Pixmap*(__thiscall*)(void*, const imvu::Pixmap*);
|
||||
using Destructor = void(__thiscall*)(imvu::Pixmap*);
|
||||
|
||||
struct Methods {
|
||||
Getter width = nullptr;
|
||||
Getter height = nullptr;
|
||||
Getter rowBytes = nullptr;
|
||||
PixelsGetter pixels = nullptr;
|
||||
SizeConstructor construct = nullptr;
|
||||
DefaultConstructor constructEmpty = nullptr;
|
||||
CopyConstructor copy = nullptr;
|
||||
Destructor destroy = 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"));
|
||||
found.construct = reinterpret_cast<SizeConstructor>(GetProcAddress(module, "??0Pixmap@@QAE@HH@Z"));
|
||||
found.constructEmpty = reinterpret_cast<DefaultConstructor>(GetProcAddress(module, "??0Pixmap@@QAE@XZ"));
|
||||
found.copy = reinterpret_cast<CopyConstructor>(GetProcAddress(module, "??0Pixmap@@QAE@ABV0@@Z"));
|
||||
found.destroy = reinterpret_cast<Destructor>(GetProcAddress(module, "??1Pixmap@@QAE@XZ"));
|
||||
}
|
||||
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)};
|
||||
}
|
||||
|
||||
OwnedPixmap::OwnedPixmap(int width, int height)
|
||||
{
|
||||
const Methods& m = methods();
|
||||
if (m.construct != nullptr && m.constructEmpty != nullptr && m.copy != nullptr && m.destroy != nullptr) {
|
||||
m.construct(storage_, width, height);
|
||||
valid_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
OwnedPixmap::~OwnedPixmap()
|
||||
{
|
||||
if (valid_) {
|
||||
methods().destroy(reinterpret_cast<imvu::Pixmap*>(storage_));
|
||||
}
|
||||
}
|
||||
|
||||
imvu::Pixmap* OwnedPixmap::copyTo(imvu::Pixmap* result) const
|
||||
{
|
||||
const Methods& m = methods();
|
||||
if (valid_) {
|
||||
return m.copy(result, &get());
|
||||
}
|
||||
// Without pixmap.dll's methods there's nothing to construct one with (IMVU's own code would have failed already).
|
||||
return m.constructEmpty != nullptr ? m.constructEmpty(result) : result;
|
||||
}
|
||||
|
||||
} // namespace t5de
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
// IMVU's images (Pixmap), through pixmap.dll's exported methods rather than depending on their layout (only their
|
||||
// size, for making our own).
|
||||
|
||||
#include <cstddef>
|
||||
#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;
|
||||
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);
|
||||
|
||||
// A Pixmap of our own, made and destroyed by pixmap.dll.
|
||||
class OwnedPixmap {
|
||||
public:
|
||||
// Width by height; invalid if pixmap.dll's methods can't be found.
|
||||
OwnedPixmap(int width, int height);
|
||||
OwnedPixmap(const OwnedPixmap&) = delete;
|
||||
OwnedPixmap& operator=(const OwnedPixmap&) = delete;
|
||||
~OwnedPixmap();
|
||||
|
||||
[[nodiscard]] bool valid() const { return valid_; }
|
||||
[[nodiscard]] const imvu::Pixmap& get() const { return *reinterpret_cast<const imvu::Pixmap*>(storage_); }
|
||||
|
||||
// Constructs a copy in result (raw memory for a Pixmap, as for a Pixmap returned by value); an empty Pixmap if
|
||||
// this one isn't valid.
|
||||
imvu::Pixmap* copyTo(imvu::Pixmap* result) const;
|
||||
|
||||
private:
|
||||
// sizeof(Pixmap) in pixmap.dll: its constructors set 7 fields.
|
||||
alignas(4) std::byte storage_[28];
|
||||
bool valid_ = false;
|
||||
};
|
||||
|
||||
} // namespace t5de
|
||||
@@ -1,12 +1,17 @@
|
||||
// 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 "scene_hooks.hpp"
|
||||
#include "t5de/imvu/render_engine.hpp"
|
||||
|
||||
namespace {
|
||||
@@ -29,6 +34,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; }
|
||||
@@ -39,11 +57,16 @@ extern "C" imvu::SharedPtr<imvu::PiRenderEngine> __cdecl createRenderEngine(imvu
|
||||
{
|
||||
try {
|
||||
log::info("createRenderEngine: window {}", static_cast<void*>(window.get()));
|
||||
HWND hwnd = window ? window->getPlatformWindow() : nullptr;
|
||||
scene_hooks::install();
|
||||
auto inner = create_imvu_engine(std::move(window));
|
||||
if (!inner) {
|
||||
return {};
|
||||
}
|
||||
return imvu::SharedPtr<imvu::PiRenderEngine>(std::make_unique<PassthroughEngine>(std::move(inner)));
|
||||
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)), hwnd));
|
||||
} catch (const std::exception& e) {
|
||||
// Exceptions mustn't cross into IMVU's code (a different C++ runtime).
|
||||
log::info("createRenderEngine failed: {}", e.what());
|
||||
File renamed without changes.
@@ -0,0 +1,131 @@
|
||||
#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;
|
||||
}
|
||||
LARGE_INTEGER start, end, frequency;
|
||||
QueryPerformanceCounter(&start);
|
||||
process_->send(writer_.bytes(), endsFrame);
|
||||
QueryPerformanceCounter(&end);
|
||||
QueryPerformanceFrequency(&frequency);
|
||||
counters_.bytes += writer_.size();
|
||||
counters_.sendSeconds += static_cast<double>(end.QuadPart - start.QuadPart) / static_cast<double>(frequency.QuadPart);
|
||||
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::createRenderTarget(std::uint32_t width, std::uint32_t height)
|
||||
{
|
||||
const std::uint32_t id = nextId_++;
|
||||
add(protocol::Op::CreateRenderTarget, protocol::CreateRenderTarget{id, width, height});
|
||||
return id;
|
||||
}
|
||||
|
||||
std::span<const std::byte> Recorder::readBack()
|
||||
{
|
||||
return active() ? process_->readBack() : std::span<const std::byte>{};
|
||||
}
|
||||
|
||||
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;
|
||||
++counters_.bufferUpdates;
|
||||
counters_.bufferBytes += size;
|
||||
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,79 @@
|
||||
#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 <span>
|
||||
#include <utility>
|
||||
|
||||
#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);
|
||||
// Destroyed with destroyTexture.
|
||||
std::uint32_t createRenderTarget(std::uint32_t width, std::uint32_t height);
|
||||
// After flushing a frame that reads its render target back: its pixels (RendererProcess::readBack).
|
||||
std::span<const std::byte> readBack();
|
||||
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);
|
||||
|
||||
// Diagnostics: what's been sent, and the time spent sending (waiting for the renderer process to take it).
|
||||
struct Counters {
|
||||
std::uint64_t bytes = 0;
|
||||
double sendSeconds = 0;
|
||||
std::uint32_t bufferUpdates = 0;
|
||||
std::uint64_t bufferBytes = 0;
|
||||
};
|
||||
// Returns the counters since the last call.
|
||||
Counters takeCounters() { return std::exchange(counters_, {}); }
|
||||
|
||||
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;
|
||||
Counters counters_;
|
||||
};
|
||||
|
||||
// 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
|
||||
@@ -0,0 +1,244 @@
|
||||
#include "renderer_process.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "log.hpp"
|
||||
|
||||
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()
|
||||
{
|
||||
HMODULE self = nullptr;
|
||||
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(&module_folder), &self);
|
||||
wchar_t path[MAX_PATH];
|
||||
GetModuleFileNameW(self, path, MAX_PATH);
|
||||
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;
|
||||
}
|
||||
|
||||
// The client's one renderer process, and the control section (protocol::Control) engines attach their channels
|
||||
// through. Started with the first engine; started again by the next one if it has exited.
|
||||
class Host {
|
||||
public:
|
||||
static Host& get()
|
||||
{
|
||||
static Host host;
|
||||
return host;
|
||||
}
|
||||
|
||||
std::mutex& mutex() { return mutex_; }
|
||||
|
||||
// The next channel's name.
|
||||
std::wstring nextChannel() const { return std::format(L"{}-{}", name_, channels_); }
|
||||
|
||||
// Tells the renderer process about the channel just created (nextChannel()), starting it if it isn't running.
|
||||
// Returns a handle to the renderer process, the caller's to close; null if it can't.
|
||||
HANDLE attach()
|
||||
{
|
||||
if (control_ == nullptr || (!running() && !startProcess())) {
|
||||
return nullptr;
|
||||
}
|
||||
control_->channels = ++channels_;
|
||||
SetEvent(attach_);
|
||||
HANDLE process = nullptr;
|
||||
DuplicateHandle(GetCurrentProcess(), process_, GetCurrentProcess(), &process, 0, FALSE, DUPLICATE_SAME_ACCESS);
|
||||
return process;
|
||||
}
|
||||
|
||||
private:
|
||||
Host() : name_(std::format(L"Local\\t5de-renderer-{}", GetCurrentProcessId()))
|
||||
{
|
||||
mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, sizeof(protocol::Control),
|
||||
name_.c_str());
|
||||
attach_ = CreateEventW(nullptr, FALSE, FALSE, (name_ + protocol::attachEventSuffix).c_str());
|
||||
if (mapping_ != nullptr && attach_ != nullptr) {
|
||||
control_ = static_cast<protocol::Control*>(
|
||||
MapViewOfFile(mapping_, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(protocol::Control)));
|
||||
}
|
||||
if (control_ == nullptr) {
|
||||
log::info("renderer process: can't create the control section: error {}", GetLastError());
|
||||
return;
|
||||
}
|
||||
*control_ = {.version = protocol::version, .channels = 0};
|
||||
}
|
||||
|
||||
[[nodiscard]] bool running() const { return process_ != nullptr && WaitForSingleObject(process_, 0) == WAIT_TIMEOUT; }
|
||||
|
||||
bool startProcess()
|
||||
{
|
||||
if (process_ != nullptr) {
|
||||
CloseHandle(process_);
|
||||
process_ = nullptr;
|
||||
}
|
||||
const auto exe = running_copy(module_folder() / L"t5de-renderer.exe");
|
||||
std::wstring command =
|
||||
std::format(L"\"{}\" --control {} --parent {}", exe.wstring(), name_, GetCurrentProcessId());
|
||||
STARTUPINFOW startup{.cb = sizeof(STARTUPINFOW)};
|
||||
PROCESS_INFORMATION process{};
|
||||
if (!CreateProcessW(exe.c_str(), command.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &startup,
|
||||
&process)) {
|
||||
log::info("renderer process: can't start {}: error {}", exe.string(), GetLastError());
|
||||
return false;
|
||||
}
|
||||
CloseHandle(process.hThread);
|
||||
process_ = process.hProcess;
|
||||
log::info("renderer process: started {} (pid {})", exe.string(), process.dwProcessId);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::mutex mutex_;
|
||||
std::wstring name_;
|
||||
HANDLE mapping_ = nullptr;
|
||||
HANDLE attach_ = nullptr;
|
||||
protocol::Control* control_ = nullptr;
|
||||
HANDLE process_ = nullptr;
|
||||
std::uint32_t channels_ = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<RendererProcess> RendererProcess::start(HWND window, protocol::View view)
|
||||
{
|
||||
Host& host = Host::get();
|
||||
const std::scoped_lock lock(host.mutex());
|
||||
const std::wstring name = host.nextChannel();
|
||||
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, 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, size));
|
||||
renderer->ready_ = CreateEventW(nullptr, FALSE, FALSE, (name + protocol::readyEventSuffix).c_str());
|
||||
renderer->consumed_ = CreateEventW(nullptr, FALSE, TRUE, (name + protocol::consumedEventSuffix).c_str());
|
||||
renderer->read_ = CreateEventW(nullptr, FALSE, FALSE, (name + protocol::readEventSuffix).c_str());
|
||||
if (renderer->channel_ == nullptr || renderer->ready_ == nullptr || renderer->consumed_ == nullptr
|
||||
|| renderer->read_ == 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,
|
||||
.view = view,
|
||||
.capacity = protocol::channelCapacity,
|
||||
.chunkSize = 0,
|
||||
.chunkFlags = 0,
|
||||
.covering = 0,
|
||||
.readSize = 0};
|
||||
|
||||
// The renderer's window goes on top of the client's 3D view, as a child window. The client's window mustn't clip
|
||||
// its children (WS_CLIPCHILDREN, which the client creates it with): the client draws a frame when its window gets
|
||||
// WM_PAINT, which a window entirely covered by clipped children never gets (it then drew about once a second). The
|
||||
// renderer's swap chain is a flip model one, which the compositor shows above the client's drawing anyway.
|
||||
SetWindowLongW(window, GWL_STYLE, GetWindowLongW(window, GWL_STYLE) & ~WS_CLIPCHILDREN);
|
||||
renderer->process_ = host.attach();
|
||||
if (renderer->process_ == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
log::info("renderer process: drawing channel {}", narrow(name));
|
||||
return renderer;
|
||||
}
|
||||
|
||||
RendererProcess::~RendererProcess()
|
||||
{
|
||||
if (channel_ != nullptr) {
|
||||
channel_->closing = 1;
|
||||
if (ready_ != nullptr) {
|
||||
SetEvent(ready_);
|
||||
}
|
||||
UnmapViewOfFile(channel_);
|
||||
}
|
||||
for (HANDLE handle : {ready_, consumed_, read_, mapping_, process_}) {
|
||||
if (handle != nullptr) {
|
||||
CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(ready_);
|
||||
} while (!commands.empty());
|
||||
return true;
|
||||
}
|
||||
|
||||
std::span<const std::byte> RendererProcess::readBack()
|
||||
{
|
||||
if (!alive_) {
|
||||
return {};
|
||||
}
|
||||
const HANDLE waits[] = {read_, process_};
|
||||
const DWORD result = WaitForMultipleObjects(2, waits, FALSE, consumeTimeout);
|
||||
if (result != WAIT_OBJECT_0) {
|
||||
log::info("renderer process: {} reading back a frame; giving up on it",
|
||||
result == WAIT_OBJECT_0 + 1 ? "exited" : "not responding");
|
||||
alive_ = false;
|
||||
return {};
|
||||
}
|
||||
return {data_, std::min<std::size_t>(channel_->readSize, channel_->capacity)};
|
||||
}
|
||||
|
||||
} // namespace t5de
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
// The renderer process (t5de-renderer.exe, 64-bit) an engine draws through: one for the whole client, started with
|
||||
// the first engine; each engine sends it the command stream through a shared memory channel of its own
|
||||
// (include/t5de/protocol/channel.hpp), and closes the channel when it's destroyed.
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
|
||||
#include "t5de/protocol/channel.hpp"
|
||||
|
||||
namespace t5de {
|
||||
|
||||
class RendererProcess {
|
||||
public:
|
||||
// A channel to the renderer process (t5de-renderer.exe from this DLL's folder, started if it isn't running),
|
||||
// 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();
|
||||
|
||||
// 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);
|
||||
|
||||
// After sending a frame that reads its render target back: waits for the pixels (protocol::Channel), valid until
|
||||
// the next send. Empty if the renderer process couldn't, or is gone.
|
||||
std::span<const std::byte> readBack();
|
||||
|
||||
[[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 ready_ = nullptr;
|
||||
HANDLE consumed_ = nullptr;
|
||||
HANDLE read_ = nullptr;
|
||||
HANDLE process_ = nullptr;
|
||||
protocol::Channel* channel_ = nullptr;
|
||||
std::byte* data_ = nullptr;
|
||||
bool alive_ = true;
|
||||
};
|
||||
|
||||
} // namespace t5de
|
||||
@@ -0,0 +1,192 @@
|
||||
#include "resources.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "inner_timing.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
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(std::shared_ptr<Recorder> recorder, std::uint32_t targetId, imvu::Vec2i size)
|
||||
: inner_(nullptr), recorder_(std::move(recorder)), id_(targetId), size_(size), ownsId_(false)
|
||||
{
|
||||
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
|
||||
if (ownsId_) {
|
||||
recorder_->destroyTexture(id_);
|
||||
}
|
||||
}
|
||||
|
||||
imvu::Vec2i Texture::size() const { return inner_ != nullptr ? inner_->size() : size_; }
|
||||
|
||||
void Texture::updateRegions(const imvu::Pixmap& pixmap, const imvu::CompoundRegion& regions)
|
||||
{
|
||||
if (inner_ != nullptr) {
|
||||
{ const InnerCall timed; inner_->updateRegions(pixmap, regions); }
|
||||
recorder_->updateTexture(id_, pixmap); // all of it: the regions' layout isn't known
|
||||
}
|
||||
}
|
||||
|
||||
void Texture::update(const imvu::Pixmap& pixmap)
|
||||
{
|
||||
if (inner_ != nullptr) {
|
||||
{ const InnerCall timed; inner_->update(pixmap); }
|
||||
recorder_->updateTexture(id_, pixmap);
|
||||
}
|
||||
}
|
||||
|
||||
imvu::Pixmap* Texture::read(imvu::Pixmap* result) const
|
||||
{
|
||||
if (inner_ != nullptr) {
|
||||
return inner_->read(result);
|
||||
}
|
||||
// A render target's texture: not read back (not seen used); an image of its size.
|
||||
log::info("texture: a render target's texture read; not supported, returning a blank image");
|
||||
return OwnedPixmap(size_.x, size_.y).copyTo(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)
|
||||
{
|
||||
{ const InnerCall timed; inner_->update(data, stride, elementSize, elementCount); }
|
||||
recorder_->updateBuffer(id_, data, stride, elementSize, elementCount);
|
||||
}
|
||||
|
||||
void VertexBuffer::unknown2() { inner_->unknown2(); }
|
||||
|
||||
PixmapTarget::PixmapTarget(std::shared_ptr<Recorder> recorder, imvu::Vec2i size)
|
||||
: recorder_(std::move(recorder)),
|
||||
id_(recorder_->createRenderTarget(static_cast<std::uint32_t>(size.x), static_cast<std::uint32_t>(size.y))),
|
||||
size_(size), pixmap_(size.x, size.y)
|
||||
{
|
||||
add(static_cast<imvu::PiRenderTarget*>(this));
|
||||
}
|
||||
|
||||
PixmapTarget::~PixmapTarget()
|
||||
{
|
||||
remove(static_cast<imvu::PiRenderTarget*>(this));
|
||||
recorder_->destroyTexture(id_);
|
||||
}
|
||||
|
||||
imvu::Pixmap* PixmapTarget::getPixmap(imvu::Pixmap* result) const { return pixmap_.copyTo(result); }
|
||||
|
||||
void PixmapTarget::setPixels(std::span<const std::byte> pixels)
|
||||
{
|
||||
const auto into = pixmap_.valid() ? pixmap_pixels(pixmap_.get()) : std::nullopt;
|
||||
const auto width = static_cast<std::uint32_t>(size_.x);
|
||||
const auto height = static_cast<std::uint32_t>(size_.y);
|
||||
const std::size_t rowSize = std::size_t{width} * 4;
|
||||
if (!into || into->pixels == nullptr || pixels.size() < rowSize * height) {
|
||||
log::info("render target: {}x{} frame not read back ({} bytes)", width, height, pixels.size());
|
||||
return;
|
||||
}
|
||||
const std::size_t copied = std::min<std::size_t>(rowSize, std::size_t{into->width} * 4);
|
||||
auto* rows = reinterpret_cast<std::byte*>(into->pixels);
|
||||
for (std::uint32_t y = 0; y < std::min(into->height, height); ++y) {
|
||||
std::memcpy(rows + std::size_t{y} * into->rowBytes, pixels.data() + y * rowSize, copied);
|
||||
}
|
||||
}
|
||||
|
||||
TextureTarget::TextureTarget(std::shared_ptr<Recorder> recorder, imvu::Vec2i size)
|
||||
: recorder_(std::move(recorder)),
|
||||
id_(recorder_->createRenderTarget(static_cast<std::uint32_t>(size.x), static_cast<std::uint32_t>(size.y))),
|
||||
size_(size)
|
||||
{
|
||||
add(static_cast<imvu::PiRenderTarget*>(this));
|
||||
}
|
||||
|
||||
TextureTarget::~TextureTarget()
|
||||
{
|
||||
remove(static_cast<imvu::PiRenderTarget*>(this));
|
||||
recorder_->destroyTexture(id_);
|
||||
}
|
||||
|
||||
imvu::TextureArea* TextureTarget::area(imvu::TextureArea* result) const
|
||||
{
|
||||
return new (result) imvu::TextureArea{0, 0, 1, 1};
|
||||
}
|
||||
|
||||
imvu::SharedPtr<imvu::PiRenderTexture>* TextureTarget::texture(imvu::SharedPtr<imvu::PiRenderTexture>* result) const
|
||||
{
|
||||
return new (result) imvu::SharedPtr<imvu::PiRenderTexture>(
|
||||
std::unique_ptr<imvu::PiRenderTexture>(new Texture(recorder_, id_, size_)));
|
||||
}
|
||||
|
||||
} // namespace t5de
|
||||
@@ -0,0 +1,141 @@
|
||||
#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 <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
|
||||
#include "pixmap.hpp"
|
||||
|
||||
#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);
|
||||
// What's been drawn into one of our texture render targets (TextureTarget): no texture of the client renderer's,
|
||||
// and the id stays the target's.
|
||||
Texture(std::shared_ptr<Recorder> recorder, std::uint32_t targetId, imvu::Vec2i size);
|
||||
~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_;
|
||||
imvu::Vec2i size_{};
|
||||
bool ownsId_ = true;
|
||||
};
|
||||
|
||||
// Render targets the renderer process draws (made while it runs; otherwise the client renderer makes its own).
|
||||
class PixmapTarget final : public imvu::PiPixmapRenderTarget {
|
||||
public:
|
||||
PixmapTarget(std::shared_ptr<Recorder> recorder, imvu::Vec2i size);
|
||||
~PixmapTarget() override;
|
||||
|
||||
imvu::Pixmap* getPixmap(imvu::Pixmap* result) const override;
|
||||
|
||||
// Copies a frame's pixels, read back from the renderer process (rows packed, 0xAARRGGBB), into the pixmap.
|
||||
void setPixels(std::span<const std::byte> pixels);
|
||||
|
||||
[[nodiscard]] std::uint32_t id() const { return id_; }
|
||||
|
||||
private:
|
||||
std::shared_ptr<Recorder> recorder_;
|
||||
std::uint32_t id_;
|
||||
imvu::Vec2i size_;
|
||||
OwnedPixmap pixmap_;
|
||||
};
|
||||
|
||||
class TextureTarget final : public imvu::PiTextureRenderTarget {
|
||||
public:
|
||||
TextureTarget(std::shared_ptr<Recorder> recorder, imvu::Vec2i size);
|
||||
~TextureTarget() override;
|
||||
|
||||
imvu::TextureArea* area(imvu::TextureArea* result) const override;
|
||||
imvu::SharedPtr<imvu::PiRenderTexture>* texture(imvu::SharedPtr<imvu::PiRenderTexture>* result) const override;
|
||||
|
||||
[[nodiscard]] std::uint32_t id() const { return id_; }
|
||||
|
||||
private:
|
||||
std::shared_ptr<Recorder> recorder_;
|
||||
std::uint32_t id_;
|
||||
imvu::Vec2i size_;
|
||||
};
|
||||
|
||||
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, VertexBuffer, PixmapTarget or TextureTarget): the client
|
||||
// can also hand the engine objects the client renderer made itself (its render targets, and their 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
|
||||
@@ -0,0 +1,360 @@
|
||||
#include "scene_hooks.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "log.hpp"
|
||||
|
||||
namespace t5de::scene_hooks {
|
||||
|
||||
namespace {
|
||||
|
||||
// IMVU's SceneViewer::step() and ModelInstance::updateCalSkeleton(): methods, called with the object in ECX.
|
||||
using Method = void(__thiscall*)(void* self);
|
||||
|
||||
Method originalStep = nullptr;
|
||||
Method originalUpdateCalSkeleton = nullptr;
|
||||
// IMVU's CameraController::getCameraTransform(): returns a Transform by value (constructed in result).
|
||||
using GetTransform = void*(__thiscall*)(const void* self, void* result);
|
||||
GetTransform originalGetCameraTransform = nullptr;
|
||||
float cameraEasing = 0; // the share of the way to its target the camera moves at each tick
|
||||
std::atomic<std::int64_t> stepTime = 0;
|
||||
std::atomic<std::uint64_t> steps = 0;
|
||||
// How many ticks the last step covered: the client steps a scene several ticks at once when it falls behind.
|
||||
std::atomic<int> stepTicks = 1;
|
||||
std::atomic<bool> stepHooked = false;
|
||||
std::atomic<bool> poseHooked = false;
|
||||
std::atomic<bool> cameraHooked = false;
|
||||
|
||||
// zero.dll's constants: a tick, in milliseconds (33), and an animation frame, in seconds (1/30).
|
||||
const unsigned* millisecondsPerFrame = nullptr;
|
||||
const float* secondsPerFrame = nullptr;
|
||||
|
||||
LARGE_INTEGER frequency()
|
||||
{
|
||||
static const LARGE_INTEGER value = [] {
|
||||
LARGE_INTEGER f;
|
||||
QueryPerformanceFrequency(&f);
|
||||
return f;
|
||||
}();
|
||||
return value;
|
||||
}
|
||||
|
||||
// Our SceneViewer::step: records the tick, then steps as IMVU's does. __fastcall takes ECX like __thiscall (EDX unused).
|
||||
// Each scene viewer steps in turn at a tick: steps closer than 5 ms are the same tick.
|
||||
void __fastcall step(void* self, void* /*edx*/)
|
||||
{
|
||||
LARGE_INTEGER now;
|
||||
QueryPerformanceCounter(&now);
|
||||
const std::int64_t last = stepTime.load();
|
||||
if (now.QuadPart - last > frequency().QuadPart / 200) {
|
||||
++steps;
|
||||
if (last != 0 && millisecondsPerFrame != nullptr && *millisecondsPerFrame != 0) {
|
||||
const double ticks = static_cast<double>(now.QuadPart - last) * 1000.0 /
|
||||
static_cast<double>(frequency().QuadPart) / *millisecondsPerFrame;
|
||||
stepTicks = std::clamp(static_cast<int>(ticks + 0.5), 1, 5);
|
||||
}
|
||||
stepTime = now.QuadPart;
|
||||
}
|
||||
originalStep(self);
|
||||
}
|
||||
|
||||
// IMVU's objects, as far as posing between ticks reads and writes them (from SceneWindow.dll's ModelInstance::step,
|
||||
// which copies each effect's time and weight into its Cal3D animation at every tick, and updateCalSkeleton):
|
||||
// - ModelInstance: +0x2c, a flag: its skeleton must be posed again; +0x120, its animation effects, a std::list (a pointer
|
||||
// to the list's head node; a node: next, previous, then the effect).
|
||||
// - Effect: +0x34, playing (a flag); +0x3c, its time, in seconds; +0x90, its Cal3D animation.
|
||||
// - The Cal3D animation: +0x8, its time, which the mixer poses the skeleton at (CalMixer::updateSkeleton).
|
||||
constexpr std::size_t modelPoseDirty = 0x2c;
|
||||
constexpr std::size_t modelEffects = 0x120;
|
||||
constexpr std::size_t effectPlaying = 0x34;
|
||||
constexpr std::size_t effectTime = 0x3c;
|
||||
constexpr std::size_t effectAnimation = 0x90;
|
||||
constexpr std::size_t animationTime = 0x8;
|
||||
|
||||
template <class T>
|
||||
T& field(void* object, std::size_t offset)
|
||||
{
|
||||
return *reinterpret_cast<T*>(static_cast<std::byte*>(object) + offset);
|
||||
}
|
||||
|
||||
// Each effect's time at the last two ticks it was posed at, to tell an animation playing on (at any rate: the time it
|
||||
// moved per tick at the last step) from one held, looping back or just started (posed as it is).
|
||||
struct EffectHistory {
|
||||
std::uint64_t step = 0;
|
||||
float time = 0;
|
||||
float previous = -1;
|
||||
};
|
||||
std::unordered_map<void*, EffectHistory> histories;
|
||||
|
||||
// How far into the current tick the client is, from 0 to just under 1; 0 if unknown.
|
||||
float tick_fraction()
|
||||
{
|
||||
const std::int64_t last = stepTime.load();
|
||||
if (last == 0 || millisecondsPerFrame == nullptr || *millisecondsPerFrame == 0) {
|
||||
return 0;
|
||||
}
|
||||
LARGE_INTEGER now;
|
||||
QueryPerformanceCounter(&now);
|
||||
const double elapsed = static_cast<double>(now.QuadPart - last) * 1000.0 / static_cast<double>(frequency().QuadPart);
|
||||
return static_cast<float>(std::clamp(elapsed / *millisecondsPerFrame, 0.0, 0.999));
|
||||
}
|
||||
|
||||
// Our ModelInstance::updateCalSkeleton, called where ModelInstance::render poses the model before drawing it: between
|
||||
// ticks, poses it at the time between them. Each playing animation that moved on at the last step is set ahead by the
|
||||
// fraction of a tick since, at the rate it moved (up to 4 frames a tick), the skeleton posed (by IMVU's), and the
|
||||
// times put back, with the skeleton marked to be posed again, so everything else sees the tick's pose.
|
||||
void __fastcall updateCalSkeleton(void* model, void* /*edx*/)
|
||||
{
|
||||
const float fraction = tick_fraction();
|
||||
if (fraction < 0.01f || secondsPerFrame == nullptr) {
|
||||
originalUpdateCalSkeleton(model);
|
||||
return;
|
||||
}
|
||||
const std::uint64_t tick = steps.load();
|
||||
const float frame = *secondsPerFrame;
|
||||
|
||||
struct Saved {
|
||||
float* time;
|
||||
float value;
|
||||
};
|
||||
Saved saved[64];
|
||||
std::size_t count = 0;
|
||||
void* head = field<void*>(model, modelEffects);
|
||||
for (void* node = head != nullptr ? field<void*>(head, 0) : nullptr; node != nullptr && node != head && count < 64;
|
||||
node = field<void*>(node, 0)) {
|
||||
void* effect = field<void*>(node, 8);
|
||||
if (effect == nullptr || field<std::uint8_t>(effect, effectPlaying) == 0) {
|
||||
continue;
|
||||
}
|
||||
void* animation = field<void*>(effect, effectAnimation);
|
||||
if (animation == nullptr) {
|
||||
continue;
|
||||
}
|
||||
const float time = field<float>(effect, effectTime);
|
||||
EffectHistory& history = histories[effect];
|
||||
if (history.step != tick) {
|
||||
history.previous = history.step + 1 == tick ? history.time : -1;
|
||||
history.time = time;
|
||||
history.step = tick;
|
||||
}
|
||||
const float perTick = (history.time - history.previous) / static_cast<float>(stepTicks.load());
|
||||
if (history.previous < 0 || perTick <= frame * 0.01f || perTick > frame * 4) {
|
||||
continue; // held, looped back, or just started: as it is
|
||||
}
|
||||
float& posedTime = field<float>(animation, animationTime);
|
||||
saved[count++] = {&posedTime, posedTime};
|
||||
posedTime = time + fraction * perTick;
|
||||
}
|
||||
if (count == 0) {
|
||||
originalUpdateCalSkeleton(model);
|
||||
return;
|
||||
}
|
||||
field<std::uint8_t>(model, modelPoseDirty) = 1;
|
||||
originalUpdateCalSkeleton(model);
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
*saved[i].time = saved[i].value;
|
||||
}
|
||||
field<std::uint8_t>(model, modelPoseDirty) = 1;
|
||||
|
||||
// Forget effects not posed for a while (they may be gone).
|
||||
if (histories.size() > 1024) {
|
||||
std::erase_if(histories, [tick](const auto& entry) { return entry.second.step + 30 < tick; });
|
||||
}
|
||||
}
|
||||
|
||||
// The camera (SceneWindow.dll's CameraController::step, at each tick): each of these values moves a share k of the way
|
||||
// to its target (k is 0.4): the camera's position (+0x118, 3 floats, to +0xf0; panning moves the target), and its
|
||||
// orbit angles and zoom, in degrees (+0x140, 3 floats, to +0x134; the difference taken between -180 and 180).
|
||||
// A fraction f of a tick on, the same easing has moved it 1 - (1 - k)^f of the way: exactly, nothing guessed. The point
|
||||
// the camera looks at (+0x160, 3 floats) is the position plus an offset (setCameraFocus, at each tick), so it moves with
|
||||
// the position.
|
||||
constexpr std::size_t cameraPosition = 0x118;
|
||||
constexpr std::size_t cameraPositionTarget = 0xf0;
|
||||
constexpr std::size_t cameraAngles = 0x140;
|
||||
constexpr std::size_t cameraAnglesTarget = 0x134;
|
||||
constexpr std::size_t cameraFocus = 0x160;
|
||||
// Nonzero: the camera shows a fixed view ("take"), not eased.
|
||||
constexpr std::size_t cameraTake = 0xec;
|
||||
|
||||
float degrees180(float degrees)
|
||||
{
|
||||
float wrapped = std::fmod(degrees + 180.0f, 360.0f);
|
||||
if (wrapped < 0) {
|
||||
wrapped += 360.0f;
|
||||
}
|
||||
return wrapped - 180.0f;
|
||||
}
|
||||
|
||||
// Our CameraController::getCameraTransform: between ticks, the camera where its easing has brought it since the tick.
|
||||
// The eased values are set ahead for IMVU's code to make the transform from, then put back. __fastcall takes ECX like
|
||||
// __thiscall, and like it the callee pops the stack argument.
|
||||
void* __fastcall getCameraTransform(void* camera, void* /*edx*/, void* result)
|
||||
{
|
||||
const float fraction = tick_fraction();
|
||||
if (fraction < 0.01f || field<std::int32_t>(camera, cameraTake) != 0) {
|
||||
return originalGetCameraTransform(camera, result);
|
||||
}
|
||||
const float moved = 1.0f - std::pow(1.0f - cameraEasing, fraction);
|
||||
float saved[9];
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
float& position = field<float>(camera, cameraPosition + i * 4);
|
||||
float& focus = field<float>(camera, cameraFocus + i * 4);
|
||||
saved[i] = position;
|
||||
saved[6 + i] = focus;
|
||||
const float step = (field<float>(camera, cameraPositionTarget + i * 4) - position) * moved;
|
||||
position += step;
|
||||
focus += step;
|
||||
float& angle = field<float>(camera, cameraAngles + i * 4);
|
||||
saved[3 + i] = angle;
|
||||
angle = degrees180(angle + degrees180(field<float>(camera, cameraAnglesTarget + i * 4) - angle) * moved);
|
||||
}
|
||||
void* transform = originalGetCameraTransform(camera, result);
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
field<float>(camera, cameraPosition + i * 4) = saved[i];
|
||||
field<float>(camera, cameraAngles + i * 4) = saved[3 + i];
|
||||
field<float>(camera, cameraFocus + i * 4) = saved[6 + i];
|
||||
}
|
||||
return transform;
|
||||
}
|
||||
|
||||
// Replaces the vtable entry at slot, if it holds expected; returns whether it did.
|
||||
bool replace(void** slot, void* expected, void* replacement)
|
||||
{
|
||||
if (*slot != expected) {
|
||||
return false;
|
||||
}
|
||||
DWORD protection = 0;
|
||||
if (!VirtualProtect(slot, sizeof(void*), PAGE_READWRITE, &protection)) {
|
||||
return false;
|
||||
}
|
||||
*slot = replacement;
|
||||
VirtualProtect(slot, sizeof(void*), protection, &protection);
|
||||
FlushInstructionCache(GetCurrentProcess(), slot, sizeof(void*));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Redirects the call instruction (E8, then the target relative to the next instruction) at site, if it calls
|
||||
// expected; returns whether it did.
|
||||
bool redirect_call(std::byte* site, void* expected, void* replacement)
|
||||
{
|
||||
std::int32_t offset = 0;
|
||||
std::memcpy(&offset, site + 1, sizeof(offset));
|
||||
if (site[0] != std::byte{0xe8} || site + 5 + offset != expected) {
|
||||
return false;
|
||||
}
|
||||
const auto replacementOffset = static_cast<std::int32_t>(static_cast<std::byte*>(replacement) - (site + 5));
|
||||
DWORD protection = 0;
|
||||
if (!VirtualProtect(site, 5, PAGE_EXECUTE_READWRITE, &protection)) {
|
||||
return false;
|
||||
}
|
||||
std::memcpy(site + 1, &replacementOffset, sizeof(replacementOffset));
|
||||
VirtualProtect(site, 5, protection, &protection);
|
||||
FlushInstructionCache(GetCurrentProcess(), site, 5);
|
||||
return true;
|
||||
}
|
||||
|
||||
void install_step(HMODULE sceneWindow)
|
||||
{
|
||||
auto* vtable = reinterpret_cast<void**>(GetProcAddress(sceneWindow, "??_7SceneViewer@@6BIFrameListener@@@"));
|
||||
auto* stepMethod = GetProcAddress(sceneWindow, "?step@SceneViewer@@UAEXXZ");
|
||||
// IFrameListener's vtable: its destructor, then step (what stepFrame calls).
|
||||
if (vtable == nullptr || stepMethod == nullptr) {
|
||||
log::info("scene hooks: SceneViewer's vtable or step not found");
|
||||
return;
|
||||
}
|
||||
originalStep = reinterpret_cast<Method>(stepMethod);
|
||||
if (!replace(&vtable[1], reinterpret_cast<void*>(stepMethod), reinterpret_cast<void*>(&step))) {
|
||||
log::info("scene hooks: SceneViewer's vtable doesn't hold its step");
|
||||
return;
|
||||
}
|
||||
stepHooked = true;
|
||||
}
|
||||
|
||||
void install_pose(HMODULE sceneWindow)
|
||||
{
|
||||
HMODULE zero = GetModuleHandleW(L"zero.dll");
|
||||
millisecondsPerFrame =
|
||||
zero == nullptr ? nullptr : reinterpret_cast<const unsigned*>(GetProcAddress(zero, "?millisecondsPerFrame@@3IB"));
|
||||
secondsPerFrame = zero == nullptr ? nullptr : reinterpret_cast<const float*>(GetProcAddress(zero, "?secondsPerFrame@@3MB"));
|
||||
auto* render = reinterpret_cast<std::byte*>(GetProcAddress(
|
||||
sceneWindow, "?render@ModelInstance@@QAEXPAVRenderManager@@ABVCullingFrustum@@ABVTransform@@2ABUFogState@@"
|
||||
"ABULightState@@_NAAVSceneStats@@ABVRenderSettings@@@Z"));
|
||||
auto* update = GetProcAddress(sceneWindow, "?updateCalSkeleton@ModelInstance@@AAEXXZ");
|
||||
if (millisecondsPerFrame == nullptr || secondsPerFrame == nullptr || render == nullptr || update == nullptr) {
|
||||
log::info("scene hooks: ModelInstance::render, updateCalSkeleton or zero.dll's frame lengths not found");
|
||||
return;
|
||||
}
|
||||
// ModelInstance::render poses the model first: its 5th instruction, at +0x12, calls updateCalSkeleton.
|
||||
originalUpdateCalSkeleton = reinterpret_cast<Method>(update);
|
||||
if (!redirect_call(render + 0x12, reinterpret_cast<void*>(update), reinterpret_cast<void*>(&updateCalSkeleton))) {
|
||||
log::info("scene hooks: ModelInstance::render doesn't call updateCalSkeleton where expected");
|
||||
return;
|
||||
}
|
||||
poseHooked = true;
|
||||
}
|
||||
|
||||
void install_camera(HMODULE sceneWindow)
|
||||
{
|
||||
auto* vtable = reinterpret_cast<void**>(GetProcAddress(sceneWindow, "??_7CameraController@@6B@"));
|
||||
auto* get = GetProcAddress(sceneWindow, "?getCameraTransform@CameraController@@UBE?AVTransform@@XZ");
|
||||
auto* step = reinterpret_cast<const std::byte*>(GetProcAddress(sceneWindow, "?step@CameraController@@UAEXXZ"));
|
||||
if (vtable == nullptr || get == nullptr || step == nullptr) {
|
||||
log::info("scene hooks: CameraController's vtable, getCameraTransform or step not found");
|
||||
return;
|
||||
}
|
||||
// The easing share: a float constant CameraController::step reads (at SceneWindow.dll's 0x100cb934 when loaded at
|
||||
// its preferred address), checked to be referenced by step's code and to be the expected 0.4.
|
||||
const auto* base = reinterpret_cast<const std::byte*>(sceneWindow);
|
||||
const auto* easing = reinterpret_cast<const float*>(base + 0xcb934);
|
||||
const auto address = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(easing));
|
||||
bool referenced = false;
|
||||
for (std::size_t i = 0; i + 4 <= 0x700 && !referenced; ++i) {
|
||||
referenced = std::memcmp(step + i, &address, 4) == 0;
|
||||
}
|
||||
if (!referenced || *easing != 0.4f) {
|
||||
log::info("scene hooks: CameraController::step's easing not where expected");
|
||||
return;
|
||||
}
|
||||
cameraEasing = *easing;
|
||||
// CameraController's vtable: getCameraTransform is its 6th entry.
|
||||
originalGetCameraTransform = reinterpret_cast<GetTransform>(get);
|
||||
if (!replace(&vtable[5], reinterpret_cast<void*>(get), reinterpret_cast<void*>(&getCameraTransform))) {
|
||||
log::info("scene hooks: CameraController's vtable doesn't hold its getCameraTransform");
|
||||
return;
|
||||
}
|
||||
cameraHooked = true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void install()
|
||||
{
|
||||
static std::once_flag once;
|
||||
std::call_once(once, [] {
|
||||
HMODULE sceneWindow = GetModuleHandleW(L"SceneWindow.dll");
|
||||
if (sceneWindow == nullptr) {
|
||||
log::info("scene hooks: SceneWindow.dll not loaded");
|
||||
return;
|
||||
}
|
||||
install_step(sceneWindow);
|
||||
if (stepHooked) { // drawing between ticks needs the ticks
|
||||
install_pose(sceneWindow);
|
||||
install_camera(sceneWindow);
|
||||
}
|
||||
log::info("scene hooks: ticks {}, posing between ticks {}, camera between ticks {}",
|
||||
stepHooked ? "recorded" : "not recorded", poseHooked ? "on" : "off", cameraHooked ? "on" : "off");
|
||||
});
|
||||
}
|
||||
|
||||
bool installed() { return stepHooked; }
|
||||
|
||||
std::int64_t lastStep() { return stepTime; }
|
||||
|
||||
} // namespace t5de::scene_hooks
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
// Our hooks into SceneWindow.dll's scene code, installed once, the first time an engine is made. Each replaces where
|
||||
// IMVU's code calls a function (an entry of an exported vtable, or a call instruction's target), after checking it
|
||||
// calls the function expected, with ours, which calls IMVU's: nothing else is changed, and nothing is if a check
|
||||
// fails (an unexpected SceneWindow.dll: the hooks' features are then off, and the client runs as it would without).
|
||||
//
|
||||
// - SceneViewer::step (the IFrameListener vtable): the client's tick, 30 times a second (SceneWindow.dll's runApp
|
||||
// calls stepFrame, which steps each scene viewer). Recorded, for the frame pacer and for drawing between ticks.
|
||||
// - ModelInstance::render's call to updateCalSkeleton, which poses the model before it's drawn: between ticks, the
|
||||
// model is posed at the time between them (its animations' times moved on by the fraction of a frame since the
|
||||
// tick; Cal3D poses between keyframes), then its animations are put back as they were.
|
||||
// - CameraController::getCameraTransform (its exported vtable): between ticks, the camera where its easing toward its
|
||||
// target has brought it since the tick.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace t5de::scene_hooks {
|
||||
|
||||
void install();
|
||||
|
||||
// Whether the hooks are installed.
|
||||
[[nodiscard]] bool installed();
|
||||
|
||||
// When a scene viewer last stepped (QueryPerformanceCounter), or 0 if none has.
|
||||
[[nodiscard]] std::int64_t lastStep();
|
||||
|
||||
} // namespace t5de::scene_hooks
|
||||
@@ -0,0 +1,108 @@
|
||||
# Architecture
|
||||
|
||||
The renderer is two programs:
|
||||
|
||||
- **The adapter**, `t5deRenderEngine.dll` (32-bit, `adapter/`): a renderer plugin IMVU Classic loads in place of its
|
||||
OpenGL renderer (selected by the `t5de-renderer` patch in the `t5de` repository). It implements IMVU's
|
||||
`PiRenderEngine` interface (`include/t5de/imvu/`, `docs/interface.md`) and passes what the client draws to the
|
||||
renderer process. It stays thin: IMVU's process is 32-bit (4 GB of address space shared with Python, Gecko and
|
||||
IMVU's own code) and tied to Visual C++ 2010's ABI.
|
||||
- **The renderer process**, `t5de-renderer.exe` (64-bit, `renderer/`): draws, with
|
||||
[Diligent Engine](https://github.com/DiligentGraphics/DiligentCore) (Apache-2.0). Being 64-bit and separate, it can
|
||||
use any modern API and library, isn't limited by the client's address space, and can crash without taking the
|
||||
client down.
|
||||
|
||||
Why Diligent: one API over Direct3D 11 (every Windows PC, and software rendering on machines without a graphics
|
||||
driver), Direct3D 12 and Vulkan (ray tracing, mesh shaders), and WebGPU (browsers, for a possible future client).
|
||||
It's built with CMake and fetched by `FetchContent`, pinned to a commit (`renderer/CMakeLists.txt`); only the
|
||||
Direct3D 11 backend is built for now.
|
||||
|
||||
## How they talk
|
||||
|
||||
`include/t5de/protocol/` is shared by both builds, so it uses fixed-size fields only. One renderer process draws all
|
||||
of a client's 3D views, with one Direct3D device. The adapter starts `t5de-renderer.exe --control <name> --parent
|
||||
<pid>` from its own folder with the first engine (again with the next one if it has exited), and creates a control
|
||||
section (`Local\t5de-renderer-<pid>`, `protocol::Control`). For each engine the client creates (each 3D view: Home's
|
||||
avatar, the room, ...), the adapter:
|
||||
|
||||
1. creates a shared memory channel (`<control name>-<n>`) holding IMVU's window handle, and its events;
|
||||
2. counts it in the control section and sets `<control name>-attach`, and the renderer process opens it, with a
|
||||
child window, swap chain and scene of its own;
|
||||
3. sends each frame's commands through it, and sets `closing` when the engine is destroyed (the renderer process
|
||||
then closes the view).
|
||||
|
||||
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. 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 client exits.
|
||||
|
||||
## Status
|
||||
|
||||
The renderer process draws the whole 3D view in place of the client's renderer (the full view, the default). The
|
||||
adapter still forwards state and resources to the client's Direct3D renderer (as a fallback, and for reading textures
|
||||
back, which the renderer process doesn't do), but while the renderer process covers the whole view
|
||||
(`Channel::covering`, set after each frame it presents) the client's renderer skips its clears and draws, decided per
|
||||
frame; it draws again as soon as the renderer process stops covering the view (the view changes, or the process
|
||||
fails). Checked in rooms with screenshots of both renderers side by side (the compare view): they match, particles
|
||||
and the 2D overlay (name tags) included.
|
||||
|
||||
For comparing while the client runs (development):
|
||||
|
||||
- Views: `T5DE_RENDERER_VIEW` (off, split, full (the default) or compare) when the client starts, or
|
||||
`%TEMP%\t5de-renderer-view.txt` holding one of those at any time. Compare draws the left half's scene over the
|
||||
right half, so both renderers show the same thing side by side.
|
||||
- `%TEMP%\t5de-renderer-shaders\fixed_function.hlsl`, if present, replaces the built-in shaders and is reloaded
|
||||
when it changes.
|
||||
- The renderer process runs from a copy (`%TEMP%\t5de-renderer`), so `t5de-renderer.exe` can be replaced while the
|
||||
client runs; engines started afterwards run the new one (the room's view keeps its engine until the client
|
||||
restarts).
|
||||
- The adapter's log (`%TEMP%\t5de-renderer.log`) sums up what the client draws every 300 frames (the first few
|
||||
times), and logs textures and render targets the client renderer made itself.
|
||||
|
||||
- The command stream (`include/t5de/protocol/commands.hpp`) carries state, transforms, lights, fog, textures,
|
||||
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.
|
||||
|
||||
Render targets are the renderer process's too, while it runs: the adapter makes its own (`PixmapTarget`,
|
||||
`TextureTarget`), the renderer process draws frames into them (textures in the window's formats, so the same
|
||||
pipelines draw both), and a pixmap target's frame is read back through the channel (`Channel::readSize`, the
|
||||
`-read` event) into its Pixmap, as the client's renderer does, synchronously. A read back must fit in the channel's
|
||||
data area (16 MB: the client's 2048x2048 snapshots just do). Checked with outfit pictures and photos.
|
||||
|
||||
Higher frame rates (in progress): the client steps its scene 30 times a second (SceneWindow.dll's `runApp`), and
|
||||
draws a window's frame on WM_PAINT. The adapter's frame pacer (`adapter/frame_pacer.cpp`) asks the 3D windows to
|
||||
repaint between steps at the user's rate (T5DE's settings, Graphics: `HKCU\Software\T5DE\Graphics\FrameRate`), only
|
||||
leaving the client idle time, and never holding up the next step: the adapter records the client's steps by replacing
|
||||
`SceneViewer::step`'s entry in its exported vtable (`adapter/scene_hooks.cpp`, after checking it), and a frame that
|
||||
wouldn't end before the step is due isn't asked for (a late step makes the client catch up two at once: hitches).
|
||||
While the renderer process covers the view, the client's renderer doesn't begin or present frames (its present can
|
||||
wait for the display). The renderer process takes each view's chunks on a thread of its own as they come, so the
|
||||
client never waits for drawing; it draws the newest frame. On a PC with a graphics card a room frame takes the client
|
||||
about 2.5 ms (the client's own work: posing avatars, which it does every frame, and walking the scene), and the
|
||||
renderer process about 0.5 ms.
|
||||
|
||||
Between steps, the frames show the scene at the time between them (`adapter/scene_hooks.cpp`): avatars posed with
|
||||
their animations a fraction of a frame on (ModelInstance::render's call to updateCalSkeleton, redirected; Cal3D poses
|
||||
between keyframes; animations held, looping or starting are posed as they are), and the camera where its easing (a
|
||||
share 0.4 of the way to its target per step) has brought it, `1 - 0.6^fraction` of the way (CameraController's
|
||||
getCameraTransform, in its exported vtable). Nothing is predicted: both are what the next step would give, at the
|
||||
fraction. Still at 30 per second: particles, crossfades between animations, gaze.
|
||||
|
||||
Next: sharing pipelines between views (each view's scene compiles its own).
|
||||
|
||||
## Building
|
||||
|
||||
Two presets, one per architecture: `cmake --workflow --preset adapter` (32-bit, with the tests) and
|
||||
`cmake --workflow --preset renderer` (64-bit; the first configure fetches and builds Diligent). Both programs go in
|
||||
the client's folder.
|
||||
+33
-3
@@ -59,9 +59,39 @@ virtual methods, IMVU's code only uses an engine through helpers that call its v
|
||||
`destroy`, `get_deleter`, `get_untyped_deleter`), the use count and the weak count, changed atomically.
|
||||
`include/t5de/imvu/shared_ptr.hpp` matches it. With non-trivial copy and destruction, it's returned through a
|
||||
hidden pointer and passed on the stack, as IMVU's code expects.
|
||||
- Still to work out, for a renderer of our own: `Mat4f`, `Transform`, `LightProperties`, `TextureTransform`,
|
||||
`Pixmap` (`pixmap.dll`), the buffers' contents, `VertexAttributeArray<T>`, `std::vector<PointSprite>` as
|
||||
Visual C++ 2010 lays it out, and `RenderBlendMode`'s values.
|
||||
- The data types are in `include/t5de/imvu/types.hpp`, worked out from the Direct3D engine: matrices and
|
||||
transforms in Direct3D's row-vector convention, colors as `0xAARRGGBB`, lights (point or directional), blend
|
||||
modes, linear fog, texture coordinate transforms, vertex attribute arrays (data, count, stride). `Pixmap` is used
|
||||
through `pixmap.dll`'s exported methods (`width`, `height`, `pitch`, `pixels`, ...), not its layout.
|
||||
- `drawTriangles`' vertex buffers are positions (`Vec3f`), normals (`Vec3f`), texture coordinates (`Vec2f`) and
|
||||
colors; vertex buffers are made with a stride, element size and count, dynamic ones filled later through the
|
||||
buffer's own `update`. The objects an engine creates (buffers, textures, render targets) are declared in
|
||||
`include/t5de/imvu/resources.hpp`; IMVU's code deletes them through their virtual destructors.
|
||||
- Checked in the client with the pass-through's logged arguments: a right-handed perspective projection (45 degree
|
||||
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.
|
||||
- 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.
|
||||
- Render targets (`include/t5de/imvu/resources.hpp`): the engine returns each as a pointer into its own object;
|
||||
the client calls only their interfaces' methods and passes them to `beginRendering`. A pixmap render target has one
|
||||
method, `getPixmap()` (a Pixmap returned by value), with the pixels of the last frame drawn into it (IMVU's engine
|
||||
copies them from the GPU at the frame's end). A texture render target has `area()` (always 0, 0, 1, 1) and
|
||||
`texture()` (a new texture object for what's been drawn, as a shared pointer returned by value). The client uses
|
||||
pixmap targets for snapshots (`takeSnapshot`: outfit pictures, the photo widget), 2048x2048; texture targets
|
||||
haven't been seen used. Pixmap is 28 bytes; pixmap.dll exports its constructors and destructor.
|
||||
- Still unknown: `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
|
||||
own C runtime. Exceptions must not cross it.
|
||||
|
||||
|
||||
@@ -1,54 +1,16 @@
|
||||
#pragma once
|
||||
// IMVU's renderer interface, PiRenderEngine (SceneWindow.dll), declared to match its vtable: the order of the methods
|
||||
// and their parameter types are what matter. The names are IMVU's, from SceneWindow.dll's exported RenderEngineDx
|
||||
// methods; parameter names are ours, and guesses until the calls are traced. Types used only through pointers or
|
||||
// references are left incomplete until we need their layout. See docs/interface.md.
|
||||
// methods; parameter names are ours, from what IMVU's Direct3D engine does with them. See docs/interface.md.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "t5de/imvu/resources.hpp"
|
||||
#include "t5de/imvu/shared_ptr.hpp"
|
||||
#include "t5de/imvu/types.hpp"
|
||||
|
||||
namespace t5de::imvu {
|
||||
|
||||
template <class T>
|
||||
struct Vec2 {
|
||||
T x;
|
||||
T y;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
struct Vec3 {
|
||||
T x;
|
||||
T y;
|
||||
T z;
|
||||
};
|
||||
|
||||
using Vec2i = Vec2<int>;
|
||||
using Vec2f = Vec2<float>;
|
||||
using Vec3f = Vec3<float>;
|
||||
|
||||
struct Mat4f;
|
||||
struct Transform;
|
||||
struct LightProperties;
|
||||
struct TextureTransform;
|
||||
struct PointSprite;
|
||||
class Pixmap;
|
||||
class INativeWindow;
|
||||
class PiRenderTexture;
|
||||
class PiInternalIndexBuffer;
|
||||
class PiInternalVertexBuffer;
|
||||
class PiRenderTarget;
|
||||
class PiPixmapRenderTarget;
|
||||
class PiTextureRenderTarget;
|
||||
template <class T>
|
||||
class VertexAttributeArray;
|
||||
|
||||
// std::vector<PointSprite> as Visual C++ 2010 lays it out.
|
||||
struct PointSpriteVector;
|
||||
|
||||
// Values not yet known.
|
||||
enum class RenderBlendMode : int {};
|
||||
|
||||
class PiRenderEngine {
|
||||
public:
|
||||
PiRenderEngine() = default;
|
||||
@@ -59,7 +21,7 @@ public:
|
||||
|
||||
virtual void resizeFrameBuffer(Vec2i size) = 0;
|
||||
virtual void enable2dFrameBuffer() = 0;
|
||||
virtual void clear(std::uint32_t flags, std::uint32_t color) = 0;
|
||||
virtual void clear(std::uint32_t flags, Color color) = 0; // ClearFlags
|
||||
virtual void setProjection(Vec2i viewport, const Mat4f& projection) = 0;
|
||||
virtual void setModelSpace(const Transform& model) = 0; // slot 5
|
||||
virtual void setViewSpace(const Transform& view) = 0;
|
||||
@@ -75,29 +37,33 @@ public:
|
||||
virtual void setLightingOn(bool on) = 0;
|
||||
virtual void setLightProperties(std::uint32_t index, const LightProperties& light) = 0;
|
||||
virtual void setFogOn(bool on) = 0;
|
||||
virtual void setFogAttributes(float start, float end, std::uint32_t color) = 0;
|
||||
virtual void setFogAttributes(float start, float end, Color color) = 0;
|
||||
virtual void setTextureTransformMatrixIdentity(std::uint32_t stage) = 0; // slot 20
|
||||
virtual void setTextureTransformMatrix(std::uint32_t stage, const TextureTransform& transform) = 0;
|
||||
virtual void setEnvironmentAmbient(std::uint32_t color) = 0;
|
||||
virtual PiRenderTexture* newRenderTexture(Pixmap* pixmap, std::uint32_t flags) = 0;
|
||||
virtual void setEnvironmentAmbient(Color color) = 0; // zero alpha
|
||||
virtual PiRenderTexture* newRenderTexture(const Pixmap& pixmap, std::uint32_t flags) = 0;
|
||||
virtual PiInternalIndexBuffer* newIndexBuffer(std::uint32_t count, const std::uint16_t* indices) = 0;
|
||||
virtual PiInternalVertexBuffer* newStaticVertexBuffer(const void* data, std::uint32_t a, std::uint32_t b,
|
||||
std::uint32_t c) = 0; // slot 25
|
||||
virtual PiInternalVertexBuffer* newDynamicVertexBuffer(std::uint32_t a, std::uint32_t b) = 0;
|
||||
virtual PiInternalVertexBuffer* newStaticVertexBuffer(const void* data, std::uint32_t stride,
|
||||
std::uint32_t elementSize,
|
||||
std::uint32_t elementCount) = 0; // slot 25
|
||||
// Filled later with PiInternalVertexBuffer::update.
|
||||
virtual PiInternalVertexBuffer* newDynamicVertexBuffer(std::uint32_t elementSize, std::uint32_t elementCount) = 0;
|
||||
virtual SharedPtr<PiPixmapRenderTarget> newPixmapRenderTarget(Vec2i size) = 0;
|
||||
virtual SharedPtr<PiTextureRenderTarget> newTextureRenderTarget(Vec2i size) = 0;
|
||||
virtual void beginRendering(PiRenderTarget* target) = 0;
|
||||
virtual void endRendering() = 0; // slot 30
|
||||
// Vertex buffers: positions (Vec3f), normals (Vec3f), texture coordinates (Vec2f), colors (Color); all but
|
||||
// positions may be null.
|
||||
virtual void drawTriangles(PiInternalIndexBuffer* indices, PiInternalVertexBuffer* positions,
|
||||
PiInternalVertexBuffer* normals, PiInternalVertexBuffer* texCoords0,
|
||||
PiInternalVertexBuffer* texCoords1) = 0;
|
||||
PiInternalVertexBuffer* normals, PiInternalVertexBuffer* texCoords,
|
||||
PiInternalVertexBuffer* colors) = 0;
|
||||
virtual void drawLine(Vec3f from, Vec3f to, std::uint32_t color) = 0;
|
||||
virtual void drawPointSprites(const PointSpriteVector& sprites) = 0;
|
||||
virtual void drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount,
|
||||
const std::uint16_t* indices, const VertexAttributeArray<Vec3f>& positions,
|
||||
const VertexAttributeArray<Vec3f>& normals,
|
||||
const VertexAttributeArray<Vec2f>& texCoords,
|
||||
const VertexAttributeArray<std::uint32_t>& colors) = 0; // slot 34
|
||||
const VertexAttributeArray<Color>& colors) = 0; // slot 34
|
||||
|
||||
private:
|
||||
// PiRenderEngine's own fields, after the vtable pointer: a magic number IMVU's destructor checks, and a size
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
// The objects a PiRenderEngine creates and hands to IMVU's code: buffers, textures, render targets. IMVU's code keeps
|
||||
// them, calls their virtual methods, and deletes them through their virtual destructors. Declared from the vtables in
|
||||
// SceneWindow.dll; method names are ours, from what IMVU's implementations do. Not yet verified by a renderer of our
|
||||
// own.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "t5de/imvu/shared_ptr.hpp"
|
||||
#include "t5de/imvu/types.hpp"
|
||||
|
||||
namespace t5de::imvu {
|
||||
|
||||
class PiInternalIndexBuffer {
|
||||
public:
|
||||
virtual ~PiInternalIndexBuffer() = default;
|
||||
};
|
||||
|
||||
class PiInternalVertexBuffer {
|
||||
public:
|
||||
virtual ~PiInternalVertexBuffer() = default;
|
||||
// Replaces a dynamic buffer's contents: elementCount elements of elementSize bytes, stride bytes apart (a multiple
|
||||
// of 4, at least elementSize). Element size and count are the ones the buffer was created with.
|
||||
virtual void update(const void* data, std::uint32_t stride, std::uint32_t elementSize,
|
||||
std::uint32_t elementCount) = 0;
|
||||
// Unknown: IMVU's software buffer forgets its data; its Direct3D buffer asserts, so it isn't called there.
|
||||
virtual void unknown2() = 0;
|
||||
};
|
||||
|
||||
class PiRenderTexture {
|
||||
public:
|
||||
virtual ~PiRenderTexture() = default;
|
||||
virtual Vec2i size() const = 0;
|
||||
// Copies the parts of pixmap in regions into the texture. (Not overloaded with update: MSVC orders overloaded
|
||||
// virtual methods unlike their declaration.)
|
||||
virtual void updateRegions(const Pixmap& pixmap, const CompoundRegion& regions) = 0;
|
||||
// Copies all of pixmap into the texture.
|
||||
virtual void update(const Pixmap& pixmap) = 0;
|
||||
// Reads the texture into a new Pixmap, constructed in result (a Pixmap returned by value).
|
||||
virtual Pixmap* read(Pixmap* result) const = 0;
|
||||
};
|
||||
|
||||
// What frames can be drawn into besides the window (PiRenderEngine::beginRendering). IMVU's Direct3D engine returns
|
||||
// each as a pointer into its own object (single inheritance from these, so the PiRenderTarget pointer is the same).
|
||||
class PiRenderTarget {
|
||||
public:
|
||||
virtual ~PiRenderTarget() = default;
|
||||
};
|
||||
|
||||
// Drawn into, then read: after each frame drawn into it, getPixmap() has its pixels (IMVU's engine copies them at the
|
||||
// end of the frame). Used for snapshots: outfit pictures, the photo widget.
|
||||
class PiPixmapRenderTarget : public PiRenderTarget {
|
||||
public:
|
||||
// Constructs a copy of the pixels in result (a Pixmap returned by value).
|
||||
virtual Pixmap* getPixmap(Pixmap* result) const = 0;
|
||||
};
|
||||
|
||||
// The part of the texture the target covers, in texture coordinates. IMVU's Direct3D engine always returns 0, 0, 1, 1.
|
||||
struct TextureArea {
|
||||
float left;
|
||||
float top;
|
||||
float right;
|
||||
float bottom;
|
||||
};
|
||||
|
||||
// Drawn into, then drawn with as a texture.
|
||||
class PiTextureRenderTarget : public PiRenderTarget {
|
||||
public:
|
||||
// Constructs the area in result (returned by value).
|
||||
virtual TextureArea* area(TextureArea* result) const = 0;
|
||||
// Constructs, in result, a new texture object for what's been drawn (a shared pointer returned by value).
|
||||
virtual SharedPtr<PiRenderTexture>* texture(SharedPtr<PiRenderTexture>* result) const = 0;
|
||||
};
|
||||
|
||||
} // namespace t5de::imvu
|
||||
@@ -0,0 +1,126 @@
|
||||
#pragma once
|
||||
// The data types IMVU's renderer interface passes, as SceneWindow.dll's Direct3D 9 engine reads them. Most are passed
|
||||
// by reference: where IMVU's type might hold more than we read, its size isn't asserted. See docs/interface.md.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
struct HWND__; // windows.h's window handle type (HWND is HWND__*)
|
||||
|
||||
namespace t5de::imvu {
|
||||
|
||||
template <class T>
|
||||
struct Vec2 {
|
||||
T x;
|
||||
T y;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
struct Vec3 {
|
||||
T x;
|
||||
T y;
|
||||
T z;
|
||||
};
|
||||
|
||||
using Vec2i = Vec2<int>;
|
||||
using Vec2f = Vec2<float>;
|
||||
using Vec3f = Vec3<float>;
|
||||
|
||||
// Colors are 0xAARRGGBB (Direct3D's D3DCOLOR). Light and ambient colors must have zero alpha.
|
||||
using Color = std::uint32_t;
|
||||
|
||||
// A 4x4 matrix in Direct3D's convention: row-major, transforming row vectors (p' = p * M).
|
||||
struct Mat4f {
|
||||
float m[4][4];
|
||||
};
|
||||
|
||||
// An affine transform: a 3x3 matrix (row-major), then a translation; p' = p * linear + translation, as in Direct3D.
|
||||
// The Direct3D engine expands it to [linear 0; translation 1]. (What it reads; IMVU's type may be larger.)
|
||||
struct Transform {
|
||||
float linear[3][3];
|
||||
Vec3f translation;
|
||||
};
|
||||
|
||||
// A texture coordinate transform: u' = u * m[0] + v * m[1] + m[4], v' = u * m[2] + v * m[3] + m[5].
|
||||
struct TextureTransform {
|
||||
float m[6];
|
||||
};
|
||||
|
||||
enum class LightType : int {
|
||||
Point = 0, // at `vector`; the Direct3D engine sets no attenuation and a fixed range
|
||||
Directional = 1, // shining along -`vector` (a zero vector means along -x)
|
||||
};
|
||||
|
||||
// What the Direct3D engine reads of a light. (IMVU's type may be larger.)
|
||||
struct LightProperties {
|
||||
bool enabled;
|
||||
Color diffuse; // zero alpha
|
||||
Vec3f vector;
|
||||
LightType type;
|
||||
};
|
||||
|
||||
static_assert(offsetof(LightProperties, diffuse) == 4 && offsetof(LightProperties, vector) == 8
|
||||
&& offsetof(LightProperties, type) == 0x14);
|
||||
|
||||
// Blend functions, as source and destination factors.
|
||||
enum class RenderBlendMode : int {
|
||||
Alpha = 1, // source alpha, inverse source alpha
|
||||
Additive = 2, // one, one
|
||||
Premultiplied = 3, // one, inverse source alpha
|
||||
AdditiveAlpha = 4, // source alpha, one
|
||||
};
|
||||
|
||||
// clear()'s flags.
|
||||
enum ClearFlags : std::uint32_t {
|
||||
ClearColor = 1,
|
||||
ClearDepth = 2, // to 1.0
|
||||
};
|
||||
|
||||
// Vertex attributes in memory: count elements, stride bytes apart (0: packed).
|
||||
template <class T>
|
||||
struct VertexAttributeArray {
|
||||
const T* data;
|
||||
std::uint32_t count;
|
||||
std::uint32_t stride;
|
||||
};
|
||||
|
||||
static_assert(sizeof(VertexAttributeArray<Vec3f>) == 12);
|
||||
|
||||
// IMVU's image class (pixmap.dll): 32-bit pixels (Color). Its layout isn't needed: pixmap.dll exports its methods
|
||||
// (width, height, pitch, rowBytes, pixels, ...).
|
||||
class Pixmap;
|
||||
|
||||
// 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;
|
||||
|
||||
struct UiEvent;
|
||||
using NativeWindowHandle = ::HWND__*; // an HWND
|
||||
|
||||
// IMVU's window (SceneWindow.dll's NativeWindow), as far as a renderer needs it: the first methods of its vtable.
|
||||
// Its later methods include overloads, which MSVC lays out unlike their declaration: declare them from the vtable
|
||||
// if they're ever needed.
|
||||
class INativeWindow {
|
||||
public:
|
||||
virtual ~INativeWindow() = default;
|
||||
virtual void handleMessage(UiEvent* event) = 0;
|
||||
virtual int onSetCursor(Vec2i position) = 0;
|
||||
virtual NativeWindowHandle getPlatformWindow() = 0;
|
||||
};
|
||||
|
||||
} // namespace t5de::imvu
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
// What the adapter (t5deRenderEngine.dll, in IMVU's 32-bit process) and the renderer process (64-bit) share. Both
|
||||
// are built from this header, for different architectures: fixed-size fields only, no pointers or handles' native
|
||||
// sizes.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace t5de::protocol {
|
||||
|
||||
inline constexpr std::uint32_t version = 5;
|
||||
|
||||
// One renderer process draws all of a client's 3D views. A shared memory section the adapter creates once per client
|
||||
// process, named by the renderer process's `--control <name>` argument, holds this; each engine's channel is named
|
||||
// "<name>-<n>", n counting from 0. Having created a channel, the adapter increments channels and sets the event
|
||||
// "<name>-attach" (auto-reset); the renderer process opens the channels it hasn't yet (those already gone are
|
||||
// skipped), and stops drawing a channel's view when its engine sets closing. It exits with the client.
|
||||
struct Control {
|
||||
std::uint32_t version;
|
||||
std::uint32_t channels; // channels created so far
|
||||
};
|
||||
|
||||
inline constexpr wchar_t attachEventSuffix[] = L"-attach";
|
||||
|
||||
// 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 as Control says): 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).
|
||||
//
|
||||
// A frame drawn into a render target can ask for its pixels back (BeginFrame::readBack): having sent it, the adapter
|
||||
// waits for "<name>-read" (auto-reset), which the renderer process sets once it has drawn the frame and written the
|
||||
// pixels into the data area (readSize bytes: rows packed, 0xAARRGGBB; 0 if it couldn't). The adapter sends nothing
|
||||
// meanwhile, so the data area is free.
|
||||
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 readSize;
|
||||
};
|
||||
|
||||
static_assert(sizeof(Channel) == 48, "same layout in 32-bit and 64-bit builds");
|
||||
|
||||
inline constexpr std::uint32_t chunkEndsFrame = 1;
|
||||
inline constexpr std::uint32_t channelCapacity = 16 * 1024 * 1024;
|
||||
|
||||
inline constexpr wchar_t readyEventSuffix[] = L"-ready";
|
||||
inline constexpr wchar_t consumedEventSuffix[] = L"-consumed";
|
||||
inline constexpr wchar_t readEventSuffix[] = L"-read";
|
||||
|
||||
} // namespace t5de::protocol
|
||||
@@ -0,0 +1,280 @@
|
||||
#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,
|
||||
CreateRenderTarget,
|
||||
};
|
||||
|
||||
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 {
|
||||
// The render target drawn into (CreateRenderTarget's id), 0 for the window, or foreignTarget.
|
||||
std::uint32_t target;
|
||||
std::uint32_t readBack; // at the end of the frame, send the render target's pixels back (channel.hpp)
|
||||
};
|
||||
|
||||
// A frame drawn into a render target of the client renderer's own (made while the renderer process wasn't running):
|
||||
// the client renderer draws it, the renderer process only follows its changes.
|
||||
inline constexpr std::uint32_t foreignTarget = 0xffffffff;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
// A texture frames can be drawn into (BeginFrame), and drawn with like any other; destroyed with DestroyTexture.
|
||||
struct CreateRenderTarget {
|
||||
std::uint32_t id;
|
||||
std::uint32_t width;
|
||||
std::uint32_t height;
|
||||
};
|
||||
|
||||
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
|
||||
@@ -0,0 +1,40 @@
|
||||
# t5de-renderer.exe, the 64-bit renderer process, drawing with Diligent Engine.
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
# Diligent Engine's core, pinned to an exact commit. Only the Direct3D 11 backend for now; the others (Direct3D 12,
|
||||
# Vulkan for ray tracing and mesh shaders, WebGPU for browsers) are switched on as they're needed.
|
||||
set(DILIGENT_NO_DIRECT3D12 ON CACHE BOOL "" FORCE)
|
||||
set(DILIGENT_NO_VULKAN ON CACHE BOOL "" FORCE)
|
||||
set(DILIGENT_NO_OPENGL ON CACHE BOOL "" FORCE)
|
||||
set(DILIGENT_NO_WEBGPU ON CACHE BOOL "" FORCE)
|
||||
set(DILIGENT_NO_ARCHIVER ON CACHE BOOL "" FORCE)
|
||||
set(DILIGENT_NO_SUPER_RESOLUTION ON CACHE BOOL "" FORCE)
|
||||
set(DILIGENT_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(DILIGENT_INSTALL_CORE OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_Declare(DiligentCore
|
||||
GIT_REPOSITORY https://github.com/DiligentGraphics/DiligentCore.git
|
||||
GIT_TAG c555e4d26de3f414eaf792fa16c0c24827619b17 # master, 2026-10-27
|
||||
GIT_SUBMODULES ThirdParty/xxHash
|
||||
EXCLUDE_FROM_ALL
|
||||
SYSTEM)
|
||||
FetchContent_MakeAvailable(DiligentCore)
|
||||
|
||||
# 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
|
||||
@@ -0,0 +1,486 @@
|
||||
// t5de-renderer.exe: the renderer process. Started by the adapter (t5deRenderEngine.dll), one for the whole client:
|
||||
// receives each 3D view's drawing as commands through a shared memory channel of its own, and draws it with Diligent
|
||||
// into a child window it places over the view in the client's window.
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <d3d11.h>
|
||||
#include <dxgi.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#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;
|
||||
|
||||
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 control;
|
||||
DWORD parent = 0;
|
||||
};
|
||||
|
||||
Arguments parse_arguments()
|
||||
{
|
||||
Arguments arguments;
|
||||
int count = 0;
|
||||
wchar_t** argv = CommandLineToArgvW(GetCommandLineW(), &count);
|
||||
for (int i = 1; i + 1 < count; i += 2) {
|
||||
const std::wstring_view name = argv[i];
|
||||
if (name == L"--control") {
|
||||
arguments.control = argv[i + 1];
|
||||
} else if (name == L"--parent") {
|
||||
arguments.parent = static_cast<DWORD>(std::wcstoul(argv[i + 1], nullptr, 10));
|
||||
}
|
||||
}
|
||||
LocalFree(argv);
|
||||
return arguments;
|
||||
}
|
||||
|
||||
std::string narrow(const std::wstring& text) { return std::filesystem::path(text).string(); }
|
||||
|
||||
LRESULT CALLBACK window_procedure(HWND window, UINT message, WPARAM wparam, LPARAM lparam)
|
||||
{
|
||||
if (message == WM_ERASEBKGND) {
|
||||
return 1; // we draw all of it
|
||||
}
|
||||
return DefWindowProcW(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
// A child of the client's window. Disabled, so mouse input goes to the client's window under it.
|
||||
HWND create_child_window(HWND parent)
|
||||
{
|
||||
static const ATOM windowClass = [] {
|
||||
const WNDCLASSEXW description{.cbSize = sizeof(WNDCLASSEXW),
|
||||
.lpfnWndProc = window_procedure,
|
||||
.hInstance = GetModuleHandleW(nullptr),
|
||||
.hCursor = LoadCursorW(nullptr, IDC_ARROW),
|
||||
.lpszClassName = L"T5DERenderer"};
|
||||
return RegisterClassExW(&description);
|
||||
}();
|
||||
return CreateWindowExW(0, MAKEINTATOM(windowClass), L"", WS_CHILD | WS_VISIBLE | WS_DISABLED | WS_CLIPSIBLINGS, 0,
|
||||
0, 1, 1, parent, nullptr, GetModuleHandleW(nullptr), 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 std::optional<View> view;
|
||||
static ULONGLONG checked = 0;
|
||||
if (GetTickCount64() - checked >= 1000) {
|
||||
checked = GetTickCount64();
|
||||
view.reset();
|
||||
wchar_t temp[MAX_PATH + 1];
|
||||
const DWORD length = GetTempPathW(MAX_PATH + 1, temp);
|
||||
const HANDLE file = length == 0 || length > MAX_PATH
|
||||
? INVALID_HANDLE_VALUE
|
||||
: 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) {
|
||||
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 "));
|
||||
if (setting == "off") {
|
||||
view = View::Off;
|
||||
} else if (setting == "split") {
|
||||
view = View::Split;
|
||||
} else if (setting == "full") {
|
||||
view = View::Full;
|
||||
} else if (setting == "compare") {
|
||||
view = View::Compare;
|
||||
}
|
||||
}
|
||||
}
|
||||
return view.value_or(adapter);
|
||||
}
|
||||
|
||||
// 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). Returns the
|
||||
// new source when it has changed.
|
||||
std::optional<std::string> changed_shaders()
|
||||
{
|
||||
static ULONGLONG checked = 0;
|
||||
static std::filesystem::file_time_type loaded{};
|
||||
if (GetTickCount64() - checked < 1000) {
|
||||
return std::nullopt;
|
||||
}
|
||||
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 std::nullopt;
|
||||
}
|
||||
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::nullopt;
|
||||
}
|
||||
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;
|
||||
info("shaders reloaded from {}", path.string());
|
||||
return source;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// One of the client's 3D views: an engine's channel, and the child window its frames are drawn into. A thread of its
|
||||
// own takes the channel's chunks as soon as they come (so the client never waits for drawing) and queues them; the main
|
||||
// thread draws the newest complete frame, carrying out older ones' changes without drawing them.
|
||||
class ViewWindow {
|
||||
public:
|
||||
// Opens the channel named name and sets up its window; null if the channel is gone or it can't.
|
||||
static std::unique_ptr<ViewWindow> open(const std::wstring& name, IEngineFactoryD3D11* factory,
|
||||
IRenderDevice* device, IDeviceContext* context,
|
||||
const std::optional<std::string>& shaders)
|
||||
{
|
||||
std::unique_ptr<ViewWindow> view(new ViewWindow());
|
||||
view->name_ = narrow(name.substr(name.rfind(L'-') + 1));
|
||||
view->mapping_ = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, name.c_str());
|
||||
if (view->mapping_ == nullptr) {
|
||||
return nullptr; // its engine is gone already
|
||||
}
|
||||
view->channel_ = static_cast<Channel*>(MapViewOfFile(view->mapping_, FILE_MAP_ALL_ACCESS, 0, 0, 0));
|
||||
view->ready_ = OpenEventW(SYNCHRONIZE, FALSE, (name + t5de::protocol::readyEventSuffix).c_str());
|
||||
view->consumed_ = OpenEventW(EVENT_MODIFY_STATE, FALSE, (name + t5de::protocol::consumedEventSuffix).c_str());
|
||||
view->read_ = OpenEventW(EVENT_MODIFY_STATE, FALSE, (name + t5de::protocol::readEventSuffix).c_str());
|
||||
if (view->channel_ == nullptr || view->ready_ == nullptr || view->consumed_ == nullptr
|
||||
|| view->read_ == nullptr || view->channel_->version != t5de::protocol::version) {
|
||||
info("{}: can't open the channel", view->name_);
|
||||
return nullptr;
|
||||
}
|
||||
view->data_ = reinterpret_cast<std::byte*>(view->channel_ + 1);
|
||||
view->clientWindow_ = reinterpret_cast<HWND>(static_cast<std::uintptr_t>(view->channel_->window));
|
||||
view->window_ = create_child_window(view->clientWindow_);
|
||||
if (view->window_ == nullptr) {
|
||||
info("{}: can't create a window in {:#x}", view->name_, view->channel_->window);
|
||||
return nullptr;
|
||||
}
|
||||
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{view->window_}, &view->swapChain_);
|
||||
if (!view->swapChain_) {
|
||||
info("{}: can't create a swap chain", view->name_);
|
||||
return nullptr;
|
||||
}
|
||||
view->scene_ = std::make_unique<t5de::Scene>(device, context, view->swapChain_);
|
||||
if (shaders) {
|
||||
view->scene_->setShaderSource(*shaders);
|
||||
}
|
||||
// Frames read back: the adapter waits for them, sending nothing meanwhile, so the data area is free.
|
||||
ViewWindow* self = view.get();
|
||||
view->scene_->setReadBack([self](std::span<const std::byte> pixels) {
|
||||
const std::size_t size = pixels.size() <= self->channel_->capacity ? pixels.size() : 0;
|
||||
std::memcpy(self->data_, pixels.data(), size);
|
||||
self->channel_->readSize = static_cast<std::uint32_t>(size);
|
||||
SetEvent(self->read_);
|
||||
});
|
||||
QueryPerformanceFrequency(&view->frequency_);
|
||||
view->framesReady_ = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
||||
if (view->framesReady_ == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
view->receiver_ = std::thread([self] { self->receive(); });
|
||||
info("{}: drawing window {:#x}, view {}", view->name_, view->channel_->window,
|
||||
static_cast<int>(view->channel_->view));
|
||||
return view;
|
||||
}
|
||||
|
||||
ViewWindow(const ViewWindow&) = delete;
|
||||
ViewWindow& operator=(const ViewWindow&) = delete;
|
||||
|
||||
~ViewWindow()
|
||||
{
|
||||
stopping_ = true;
|
||||
if (receiver_.joinable()) {
|
||||
receiver_.join();
|
||||
}
|
||||
if (channel_ != nullptr) {
|
||||
channel_->covering = 0;
|
||||
UnmapViewOfFile(channel_);
|
||||
}
|
||||
scene_.reset();
|
||||
swapChain_.Release();
|
||||
if (window_ != nullptr) {
|
||||
DestroyWindow(window_);
|
||||
info("{}: closed", name_);
|
||||
}
|
||||
for (HANDLE handle : {ready_, consumed_, read_, mapping_, framesReady_}) {
|
||||
if (handle != nullptr) {
|
||||
CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set when complete frames are queued.
|
||||
[[nodiscard]] HANDLE framesReady() const { return framesReady_; }
|
||||
// Its engine is gone.
|
||||
[[nodiscard]] bool closing() const { return channel_->closing != 0; }
|
||||
|
||||
void setShaderSource(const std::string& source) { scene_->setShaderSource(source); }
|
||||
|
||||
// After framesReady: draws the newest complete frame queued. If the client has sent more meanwhile (drawing fell
|
||||
// behind), the older ones' changes are carried out without drawing them.
|
||||
void onFramesReady()
|
||||
{
|
||||
std::vector<std::size_t> ends;
|
||||
{
|
||||
const std::scoped_lock lock(mutex_);
|
||||
if (frameEnds_.empty()) {
|
||||
return;
|
||||
}
|
||||
const std::size_t complete = frameEnds_.back();
|
||||
frame_.assign(received_.begin(), received_.begin() + static_cast<std::ptrdiff_t>(complete));
|
||||
received_.erase(received_.begin(), received_.begin() + static_cast<std::ptrdiff_t>(complete));
|
||||
ends.swap(frameEnds_);
|
||||
}
|
||||
std::size_t begin = 0;
|
||||
for (std::size_t i = 0; i + 1 < ends.size(); ++i) {
|
||||
scene_->execute(std::span(frame_).subspan(begin, ends[i] - begin), 0, false);
|
||||
begin = ends[i];
|
||||
++skipped_;
|
||||
}
|
||||
const std::size_t complete = ends.back();
|
||||
|
||||
// Follow the client's 3D view's size.
|
||||
const View view = current_view(channel_->view);
|
||||
const auto [place, offset] = placement(clientWindow_, 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 {}", name_, width, height,
|
||||
place.left, offset);
|
||||
} else {
|
||||
ShowWindow(window_, SW_HIDE);
|
||||
channel_->covering = 0;
|
||||
info("{}: hidden", name_);
|
||||
}
|
||||
}
|
||||
LARGE_INTEGER start, end;
|
||||
QueryPerformanceCounter(&start);
|
||||
const bool covers = view == View::Full && width > 0 && height > 0;
|
||||
if (scene_->execute(std::span(frame_).subspan(begin, complete - begin), 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);
|
||||
const auto seconds = [this](LONGLONG ticks) {
|
||||
return static_cast<double>(ticks) / static_cast<double>(frequency_.QuadPart);
|
||||
};
|
||||
drawTime_ += seconds(end.QuadPart - start.QuadPart);
|
||||
latency_ += seconds(end.QuadPart - static_cast<LONGLONG>(scene_->lastSentAt()));
|
||||
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",
|
||||
name_, drawTime_ * 1000 / drawn_, latency_ * 1000 / drawn_, skipped_);
|
||||
drawTime_ = 0;
|
||||
latency_ = 0;
|
||||
drawn_ = 0;
|
||||
skipped_ = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
ViewWindow() = default;
|
||||
|
||||
// The receiver thread: takes each chunk as it comes (after "ready"), lets the adapter send the next, and queues it;
|
||||
// sets framesReady when a frame is complete. Ends when the view closes.
|
||||
void receive()
|
||||
{
|
||||
while (!stopping_ && !closing()) {
|
||||
if (WaitForSingleObject(ready_, 100) != WAIT_OBJECT_0 || closing()) {
|
||||
continue;
|
||||
}
|
||||
const std::uint32_t size = channel_->chunkSize <= channel_->capacity ? channel_->chunkSize : 0;
|
||||
const bool endsFrame = (channel_->chunkFlags & t5de::protocol::chunkEndsFrame) != 0;
|
||||
{
|
||||
const std::scoped_lock lock(mutex_);
|
||||
received_.insert(received_.end(), data_, data_ + size);
|
||||
if (endsFrame) {
|
||||
frameEnds_.push_back(received_.size());
|
||||
}
|
||||
}
|
||||
SetEvent(consumed_);
|
||||
if (endsFrame) {
|
||||
SetEvent(framesReady_);
|
||||
}
|
||||
}
|
||||
SetEvent(framesReady_); // wakes the main thread to close the view
|
||||
}
|
||||
|
||||
std::string name_;
|
||||
HANDLE mapping_ = nullptr;
|
||||
HANDLE ready_ = nullptr;
|
||||
HANDLE consumed_ = nullptr;
|
||||
HANDLE read_ = nullptr;
|
||||
Channel* channel_ = nullptr;
|
||||
std::byte* data_ = nullptr;
|
||||
HWND clientWindow_ = nullptr;
|
||||
HWND window_ = nullptr;
|
||||
RefCntAutoPtr<ISwapChain> swapChain_;
|
||||
std::unique_ptr<t5de::Scene> scene_;
|
||||
std::vector<std::byte> frame_; // the frames being drawn
|
||||
RECT placed_{};
|
||||
|
||||
std::thread receiver_;
|
||||
std::atomic<bool> stopping_ = false;
|
||||
HANDLE framesReady_ = nullptr;
|
||||
std::mutex mutex_; // guards received_ and frameEnds_
|
||||
std::vector<std::byte> received_; // chunks taken, not yet drawn
|
||||
std::vector<std::size_t> frameEnds_; // where complete frames end in received_
|
||||
|
||||
LARGE_INTEGER frequency_{};
|
||||
double drawTime_ = 0;
|
||||
double latency_ = 0;
|
||||
int drawn_ = 0;
|
||||
int skipped_ = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
|
||||
{
|
||||
const Arguments arguments = parse_arguments();
|
||||
if (arguments.control.empty()) {
|
||||
return 2;
|
||||
}
|
||||
HANDLE mapping = OpenFileMappingW(FILE_MAP_READ, FALSE, arguments.control.c_str());
|
||||
const auto* control = mapping == nullptr ? nullptr
|
||||
: static_cast<const t5de::protocol::Control*>(
|
||||
MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0));
|
||||
HANDLE attach = OpenEventW(SYNCHRONIZE, FALSE, (arguments.control + t5de::protocol::attachEventSuffix).c_str());
|
||||
HANDLE parent = OpenProcess(SYNCHRONIZE, FALSE, arguments.parent);
|
||||
if (control == nullptr || attach == nullptr || parent == nullptr || control->version != t5de::protocol::version) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
info("started for client {}", arguments.parent);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<ViewWindow>> views;
|
||||
std::uint32_t attached = 0;
|
||||
std::optional<std::string> shaders; // the override, if any
|
||||
std::size_t first = 0; // which view's event is waited on first, taking turns
|
||||
for (;;) {
|
||||
// New channels: opened in order; those whose engines are gone already are skipped.
|
||||
while (attached < control->channels) {
|
||||
if (auto view = ViewWindow::open(std::format(L"{}-{}", arguments.control, attached), factory, device,
|
||||
context, shaders)) {
|
||||
views.push_back(std::move(view));
|
||||
}
|
||||
++attached;
|
||||
}
|
||||
std::erase_if(views, [](const auto& view) { return view->closing(); });
|
||||
if (auto source = changed_shaders()) {
|
||||
shaders = std::move(source);
|
||||
for (const auto& view : views) {
|
||||
view->setShaderSource(*shaders);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<HANDLE> handles{parent, attach};
|
||||
const std::size_t count = std::min<std::size_t>(views.size(), MAXIMUM_WAIT_OBJECTS - 3);
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
handles.push_back(views[(first + i) % views.size()]->framesReady());
|
||||
}
|
||||
const DWORD result = MsgWaitForMultipleObjects(static_cast<DWORD>(handles.size()), handles.data(), FALSE, 1000,
|
||||
QS_ALLINPUT);
|
||||
MSG message;
|
||||
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
||||
DispatchMessageW(&message);
|
||||
}
|
||||
if (result == WAIT_OBJECT_0) {
|
||||
break; // the client exited
|
||||
}
|
||||
if (result >= WAIT_OBJECT_0 + 2 && result < WAIT_OBJECT_0 + handles.size()) {
|
||||
const std::size_t index = (first + (result - WAIT_OBJECT_0 - 2)) % views.size();
|
||||
views[index]->onFramesReady();
|
||||
first = (index + 1) % views.size();
|
||||
}
|
||||
}
|
||||
|
||||
views.clear();
|
||||
info("exiting");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,806 @@
|
||||
#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>()) {
|
||||
beginFrame(*begin);
|
||||
}
|
||||
break;
|
||||
case Op::EndFrame:
|
||||
if (const auto end = command->body<protocol::EndFrame>()) {
|
||||
lastSentAt_ = end->sentAt;
|
||||
}
|
||||
drew = drew || onscreen_;
|
||||
endFrame();
|
||||
break;
|
||||
case Op::Clear:
|
||||
if (const auto clear = command->body<protocol::Clear>(); clear && colorView_ != nullptr) {
|
||||
if ((clear->flags & 1) != 0) {
|
||||
float color[4];
|
||||
set_color(color, clear->color);
|
||||
context_->ClearRenderTarget(colorView_, color, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||
}
|
||||
if ((clear->flags & 2) != 0) {
|
||||
context_->ClearDepthStencil(depthView_, 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::CreateRenderTarget:
|
||||
if (const auto create = command->body<protocol::CreateRenderTarget>()) {
|
||||
createRenderTarget(*create);
|
||||
}
|
||||
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 && colorView_ != nullptr) {
|
||||
drawTriangles(*triangles);
|
||||
}
|
||||
break;
|
||||
case Op::DrawArrays:
|
||||
if (colorView_ != nullptr) {
|
||||
drawArrays(*command);
|
||||
}
|
||||
break;
|
||||
case Op::DrawPointSprites:
|
||||
if (colorView_ != nullptr) {
|
||||
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);
|
||||
projectionWidth_ = projection.width;
|
||||
projectionHeight_ = projection.height;
|
||||
setViewport();
|
||||
}
|
||||
|
||||
void Scene::setViewport()
|
||||
{
|
||||
if (colorView_ == nullptr || projectionWidth_ <= 0 || projectionHeight_ <= 0) {
|
||||
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 = onscreen_ ? static_cast<float>(-offsetX_) : 0.0f;
|
||||
viewport.TopLeftY = 0;
|
||||
viewport.Width = static_cast<float>(projectionWidth_);
|
||||
viewport.Height = static_cast<float>(projectionHeight_);
|
||||
viewport.MinDepth = 0;
|
||||
viewport.MaxDepth = 1;
|
||||
context_->SetViewports(1, &viewport, onscreen_ ? window.Width : target_->width,
|
||||
onscreen_ ? window.Height : target_->height);
|
||||
}
|
||||
|
||||
void Scene::beginFrame(const protocol::BeginFrame& begin)
|
||||
{
|
||||
onscreen_ = false;
|
||||
target_ = nullptr;
|
||||
colorView_ = nullptr;
|
||||
depthView_ = nullptr;
|
||||
readsBack_ = begin.readBack != 0;
|
||||
if (begin.target == 0) {
|
||||
if (draw_) {
|
||||
onscreen_ = true;
|
||||
colorView_ = swapChain_->GetCurrentBackBufferRTV();
|
||||
depthView_ = swapChain_->GetDepthBufferDSV();
|
||||
}
|
||||
} else if (const auto found = textures_.find(begin.target); found != textures_.end() && found->second.depth) {
|
||||
target_ = &found->second;
|
||||
colorView_ = target_->texture->GetDefaultView(TEXTURE_VIEW_RENDER_TARGET);
|
||||
depthView_ = target_->depth->GetDefaultView(TEXTURE_VIEW_DEPTH_STENCIL);
|
||||
}
|
||||
if (colorView_ != nullptr) {
|
||||
context_->SetRenderTargets(1, &colorView_, depthView_, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||
setViewport();
|
||||
}
|
||||
}
|
||||
|
||||
void Scene::endFrame()
|
||||
{
|
||||
if (readsBack_ && readBack_) {
|
||||
readBack_(target_ != nullptr ? readTarget(*target_) : std::span<const std::byte>{});
|
||||
}
|
||||
if (target_ != nullptr) {
|
||||
// Unbound, so it can be drawn with (a texture can't be both).
|
||||
context_->SetRenderTargets(0, nullptr, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||
}
|
||||
onscreen_ = false;
|
||||
target_ = nullptr;
|
||||
readsBack_ = false;
|
||||
colorView_ = nullptr;
|
||||
depthView_ = nullptr;
|
||||
}
|
||||
|
||||
std::span<const std::byte> Scene::readTarget(const Texture& target)
|
||||
{
|
||||
TextureDesc desc;
|
||||
desc.Name = "Render target read back";
|
||||
desc.Type = RESOURCE_DIM_TEX_2D;
|
||||
desc.Width = target.width;
|
||||
desc.Height = target.height;
|
||||
desc.MipLevels = 1;
|
||||
desc.Format = TEX_FORMAT_RGBA8_UNORM;
|
||||
desc.Usage = USAGE_STAGING;
|
||||
desc.CPUAccessFlags = CPU_ACCESS_READ;
|
||||
RefCntAutoPtr<ITexture> staging;
|
||||
device_->CreateTexture(desc, nullptr, &staging);
|
||||
if (!staging) {
|
||||
return {};
|
||||
}
|
||||
CopyTextureAttribs copy(target.texture, RESOURCE_STATE_TRANSITION_MODE_TRANSITION, staging,
|
||||
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||
context_->CopyTexture(copy);
|
||||
MappedTextureSubresource mapped;
|
||||
context_->MapTextureSubresource(staging, 0, 0, MAP_READ, MAP_FLAG_NONE, nullptr, mapped); // waits for the GPU
|
||||
if (mapped.pData == nullptr) {
|
||||
return {};
|
||||
}
|
||||
// RGBA in memory, to the client's 0xAARRGGBB (BGRA in memory).
|
||||
readPixels_.resize(std::size_t{target.width} * target.height * 4);
|
||||
for (std::uint32_t y = 0; y < target.height; ++y) {
|
||||
const auto* from = static_cast<const std::uint8_t*>(mapped.pData) + y * mapped.Stride;
|
||||
auto* to = reinterpret_cast<std::uint8_t*>(readPixels_.data()) + std::size_t{y} * target.width * 4;
|
||||
for (std::uint32_t x = 0; x < target.width; ++x) {
|
||||
to[x * 4 + 0] = from[x * 4 + 2];
|
||||
to[x * 4 + 1] = from[x * 4 + 1];
|
||||
to[x * 4 + 2] = from[x * 4 + 0];
|
||||
to[x * 4 + 3] = from[x * 4 + 3];
|
||||
}
|
||||
}
|
||||
context_->UnmapTextureSubresource(staging, 0, 0);
|
||||
return readPixels_;
|
||||
}
|
||||
|
||||
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::createRenderTarget(const protocol::CreateRenderTarget& create)
|
||||
{
|
||||
if (create.width == 0 || create.height == 0) {
|
||||
return;
|
||||
}
|
||||
// The window's formats, so the same pipelines draw into both.
|
||||
const SwapChainDesc& window = swapChain_->GetDesc();
|
||||
TextureDesc desc;
|
||||
desc.Name = "Client render target";
|
||||
desc.Type = RESOURCE_DIM_TEX_2D;
|
||||
desc.Width = create.width;
|
||||
desc.Height = create.height;
|
||||
desc.MipLevels = 1;
|
||||
desc.Format = window.ColorBufferFormat;
|
||||
desc.BindFlags = BIND_RENDER_TARGET | BIND_SHADER_RESOURCE;
|
||||
desc.ClearValue.Format = desc.Format;
|
||||
Texture target{.width = create.width, .height = create.height, .flags = 0};
|
||||
device_->CreateTexture(desc, nullptr, &target.texture);
|
||||
TextureDesc depth = desc;
|
||||
depth.Name = "Client render target depth";
|
||||
depth.Format = window.DepthBufferFormat;
|
||||
depth.BindFlags = BIND_DEPTH_STENCIL;
|
||||
depth.ClearValue.Format = depth.Format;
|
||||
depth.ClearValue.DepthStencil.Depth = 1;
|
||||
device_->CreateTexture(depth, nullptr, &target.depth);
|
||||
if (!target.texture || !target.depth) {
|
||||
return;
|
||||
}
|
||||
target.texture->GetDefaultView(TEXTURE_VIEW_SHADER_RESOURCE)->SetSampler(samplers_[0]);
|
||||
textures_[create.id] = std::move(target);
|
||||
}
|
||||
|
||||
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,151 @@
|
||||
#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 <functional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#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) and frames into the window aren't drawn (frames into
|
||||
// render targets always are). True if the frame was drawn into the window (to present).
|
||||
bool execute(std::span<const std::byte> commands, int offsetX, bool draw);
|
||||
|
||||
// Receives the pixels of frames that read their render target back (protocol::BeginFrame::readBack): rows packed,
|
||||
// 0xAARRGGBB; empty if they couldn't be read. Valid during the call.
|
||||
void setReadBack(std::function<void(std::span<const std::byte>)> readBack) { readBack_ = std::move(readBack); }
|
||||
|
||||
// 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;
|
||||
Diligent::RefCntAutoPtr<Diligent::ITexture> depth; // render targets only
|
||||
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 createRenderTarget(const protocol::CreateRenderTarget& create);
|
||||
void beginFrame(const protocol::BeginFrame& begin);
|
||||
void endFrame();
|
||||
// Sets the viewport for the last projection, in what the frame draws into.
|
||||
void setViewport();
|
||||
// The render target's pixels, rows packed, 0xAARRGGBB; empty if it can't be read.
|
||||
std::span<const std::byte> readTarget(const Texture& target);
|
||||
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_;
|
||||
// What the current frame draws into: the window, a render target, or nothing (a frame not drawn).
|
||||
bool onscreen_ = false; // in a frame drawn into the window
|
||||
Texture* target_ = nullptr; // in a frame drawn into this render target
|
||||
bool readsBack_ = false; // the frame's render target is read back at its end
|
||||
Diligent::ITextureView* colorView_ = nullptr;
|
||||
Diligent::ITextureView* depthView_ = nullptr;
|
||||
std::int32_t projectionWidth_ = 0;
|
||||
std::int32_t projectionHeight_ = 0;
|
||||
std::function<void(std::span<const std::byte>)> readBack_;
|
||||
std::vector<std::byte> readPixels_;
|
||||
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,290 +0,0 @@
|
||||
#include "passthrough_engine.hpp"
|
||||
|
||||
#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; }
|
||||
|
||||
} // namespace
|
||||
|
||||
PassthroughEngine::PassthroughEngine(imvu::SharedPtr<imvu::PiRenderEngine> inner) : inner_(std::move(inner))
|
||||
{
|
||||
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, std::uint32_t color)
|
||||
{
|
||||
count(3);
|
||||
inner_->clear(flags, color);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection)
|
||||
{
|
||||
count(4);
|
||||
inner_->setProjection(viewport, projection);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setModelSpace(const imvu::Transform& model)
|
||||
{
|
||||
count(5);
|
||||
inner_->setModelSpace(model);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setViewSpace(const imvu::Transform& 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)
|
||||
{
|
||||
count(10);
|
||||
inner_->setBlendMode(mode);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setAlphaTestingOn(bool on)
|
||||
{
|
||||
count(11);
|
||||
inner_->setAlphaTestingOn(on);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setAlphaTestValue(float 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)
|
||||
{
|
||||
count(17);
|
||||
inner_->setLightProperties(index, light);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setFogOn(bool on)
|
||||
{
|
||||
count(18);
|
||||
inner_->setFogOn(on);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setFogAttributes(float start, float end, std::uint32_t 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)
|
||||
{
|
||||
count(21);
|
||||
inner_->setTextureTransformMatrix(stage, transform);
|
||||
}
|
||||
|
||||
void PassthroughEngine::setEnvironmentAmbient(std::uint32_t color)
|
||||
{
|
||||
count(22);
|
||||
inner_->setEnvironmentAmbient(color);
|
||||
}
|
||||
|
||||
imvu::PiRenderTexture* PassthroughEngine::newRenderTexture(imvu::Pixmap* pixmap, std::uint32_t flags)
|
||||
{
|
||||
count(23);
|
||||
return inner_->newRenderTexture(pixmap, flags);
|
||||
}
|
||||
|
||||
imvu::PiInternalIndexBuffer* PassthroughEngine::newIndexBuffer(std::uint32_t indexCount, const std::uint16_t* indices)
|
||||
{
|
||||
count(24);
|
||||
return inner_->newIndexBuffer(indexCount, indices);
|
||||
}
|
||||
|
||||
imvu::PiInternalVertexBuffer* PassthroughEngine::newStaticVertexBuffer(const void* data, std::uint32_t a,
|
||||
std::uint32_t b, std::uint32_t c)
|
||||
{
|
||||
count(25);
|
||||
return inner_->newStaticVertexBuffer(data, a, b, c);
|
||||
}
|
||||
|
||||
imvu::PiInternalVertexBuffer* PassthroughEngine::newDynamicVertexBuffer(std::uint32_t a, std::uint32_t b)
|
||||
{
|
||||
count(26);
|
||||
return inner_->newDynamicVertexBuffer(a, b);
|
||||
}
|
||||
|
||||
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 (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* texCoords0,
|
||||
imvu::PiInternalVertexBuffer* texCoords1)
|
||||
{
|
||||
count(31);
|
||||
inner_->drawTriangles(indices, positions, normals, texCoords0, texCoords1);
|
||||
}
|
||||
|
||||
void PassthroughEngine::drawLine(imvu::Vec3f from, imvu::Vec3f to, std::uint32_t 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<std::uint32_t>& colors)
|
||||
{
|
||||
count(34);
|
||||
inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, colors);
|
||||
}
|
||||
|
||||
} // namespace t5de
|
||||
@@ -1,70 +0,0 @@
|
||||
#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.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
#include "t5de/imvu/render_engine.hpp"
|
||||
|
||||
namespace t5de {
|
||||
|
||||
class PassthroughEngine final : public imvu::PiRenderEngine {
|
||||
public:
|
||||
explicit PassthroughEngine(imvu::SharedPtr<imvu::PiRenderEngine> inner);
|
||||
~PassthroughEngine() override;
|
||||
|
||||
void resizeFrameBuffer(imvu::Vec2i size) override;
|
||||
void enable2dFrameBuffer() override;
|
||||
void clear(std::uint32_t flags, std::uint32_t color) override;
|
||||
void setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection) override;
|
||||
void setModelSpace(const imvu::Transform& model) override;
|
||||
void setViewSpace(const imvu::Transform& view) override;
|
||||
void setTextures(imvu::PiRenderTexture* texture0, imvu::PiRenderTexture* texture1) override;
|
||||
void setDepthTestingOn(bool on) override;
|
||||
void setDepthWritingOn(bool on) override;
|
||||
void setBlendMode(imvu::RenderBlendMode mode) override;
|
||||
void setAlphaTestingOn(bool on) override;
|
||||
void setAlphaTestValue(float value) override;
|
||||
void setBlendingOn(bool on) override;
|
||||
void setBackFaceCullingOn(bool on) override;
|
||||
void setNormalInvertingOn(bool on) override;
|
||||
void setLightingOn(bool on) override;
|
||||
void setLightProperties(std::uint32_t index, const imvu::LightProperties& light) override;
|
||||
void setFogOn(bool on) override;
|
||||
void setFogAttributes(float start, float end, std::uint32_t color) override;
|
||||
void setTextureTransformMatrixIdentity(std::uint32_t stage) override;
|
||||
void setTextureTransformMatrix(std::uint32_t stage, const imvu::TextureTransform& transform) override;
|
||||
void setEnvironmentAmbient(std::uint32_t color) override;
|
||||
imvu::PiRenderTexture* newRenderTexture(imvu::Pixmap* pixmap, std::uint32_t flags) override;
|
||||
imvu::PiInternalIndexBuffer* newIndexBuffer(std::uint32_t count, const std::uint16_t* indices) override;
|
||||
imvu::PiInternalVertexBuffer* newStaticVertexBuffer(const void* data, std::uint32_t a, std::uint32_t b,
|
||||
std::uint32_t c) override;
|
||||
imvu::PiInternalVertexBuffer* newDynamicVertexBuffer(std::uint32_t a, std::uint32_t b) override;
|
||||
imvu::SharedPtr<imvu::PiPixmapRenderTarget> newPixmapRenderTarget(imvu::Vec2i size) override;
|
||||
imvu::SharedPtr<imvu::PiTextureRenderTarget> newTextureRenderTarget(imvu::Vec2i size) override;
|
||||
void beginRendering(imvu::PiRenderTarget* target) override;
|
||||
void endRendering() override;
|
||||
void drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
|
||||
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords0,
|
||||
imvu::PiInternalVertexBuffer* texCoords1) override;
|
||||
void drawLine(imvu::Vec3f from, imvu::Vec3f to, std::uint32_t 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,
|
||||
const imvu::VertexAttributeArray<imvu::Vec3f>& normals,
|
||||
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
|
||||
const imvu::VertexAttributeArray<std::uint32_t>& 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);
|
||||
|
||||
imvu::SharedPtr<imvu::PiRenderEngine> inner_;
|
||||
std::array<std::uint32_t, slots> calls_{};
|
||||
std::uint64_t frames_ = 0;
|
||||
};
|
||||
|
||||
} // namespace t5de
|
||||
@@ -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