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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:18:23 -07:00
dhkatzandClaude Opus 5.5 97b1d36466 Run CI steps whatever the runner's execution policy
CI / build (push) Successful in 39s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 18:15:07 -07:00
dhkatzandClaude Opus 5.5 2d9da249fa Draw the whole 3D view in place of the client's renderer
CI / build (push) Failing after 0s
The full view is now the default. While the renderer process covers the whole
view (Channel::covering, set after each frame it presents), the adapter skips
the client's renderer's clears and draws, decided per frame; the client's
renderer still gets state and resources, and draws again as soon as the
renderer process stops covering the view.

The client's window no longer gets WS_CLIPCHILDREN: the client draws when its
window gets WM_PAINT, which a window entirely covered by clipped children never
gets (it fell to about one frame a second). The flip model swap chain shows
above the client's drawing anyway.

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 17:57:52 -07:00
dhkatzandClaude Opus 5.5 7d17cb0b1f Note the renderer's speed on real hardware
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 07:39:30 -07:00
dhkatzandClaude Opus 5.5 3cdda4c7ed Map projections' depth like the client's renderer
IMVU's projections give depth from -1 to 1; its Direct3D renderer maps it to 0 to 1, and now so does ours. Without that, the nearer half of the depth range was clipped, the whole 2D overlay (name tags, the frame rate, the room's title) with it. The adapter also draws drawTrianglesFromMemory without indices, and sums up what the client draws in its log.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 07:01:47 -07:00
dhkatzandClaude Opus 5.5 299cd8c312 Fix particle sizes, and add tools for comparing renderers
Particles: Direct3D 9's size limit evidently isn't the documented 64 pixels; unclamped (up to 2048) they match the client's renderer. A compare view shows the left half's scene over the right half, both renderers side by side; %TEMP%\t5de-renderer-view.txt switches views and %TEMP%\t5de-renderer-shaders\fixed_function.hlsl replaces the shaders while the client runs. The renderer process runs from a copy, so the program can be replaced meanwhile. The adapter recognizes only the textures and buffers it made (others reach the client renderer unchanged) and logs the others and render targets.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 06:43:34 -07:00
dhkatzandClaude Opus 5.5 1a45bf9a86 Fog by view depth again
Fogging by depth buffer value, as Direct3D 9's documentation suggests for IMVU's projections, looks nothing like the client; fogging by view depth matched it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 06:16:57 -07:00
dhkatzandClaude Opus 5.5 a04dba18c1 Draw particles, match the client's fog, and measure frame delay
Particles (point sprites) are drawn as instanced camera-facing squares sized like Direct3D 9's. Fog is by depth buffer value, as Direct3D 9 does with IMVU's projections. At most one frame is queued for display, frames carry the adapter's timestamp so the renderer process logs the delay to presenting, and the log's lines from several processes no longer mix.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 06:11:51 -07:00
dhkatzandClaude Opus 5.5 42fc55cb91 Draw the client's scene in the renderer process
The adapter records everything the client draws (state, transforms, lights, fog, textures and buffers) as a command stream, sent in chunks through the shared memory channel, while IMVU's Direct3D renderer still draws the whole view. The renderer process draws it with Diligent, with shaders reproducing IMVU's Direct3D 9 fixed-function setup, over the right half of the 3D view to compare. It skips to the newest frame when it falls behind, and logs its errors and frame times.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 05:52:15 -07:00
dhkatzandClaude Opus 5.5 5c35c8b63a Note the renderer process spike's results
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 05:22:28 -07:00
dhkatzandClaude Opus 5.5 9a7d7d2908 Add the renderer process
The renderer becomes two programs: the adapter (t5deRenderEngine.dll, 32-bit, in IMVU's process) and a 64-bit renderer process (t5de-renderer.exe) drawing with Diligent Engine, fetched with FetchContent and pinned. For now a spike: the adapter starts the renderer process for each engine and signals each frame through a shared memory channel, and the renderer process draws a box into a child window in the client's 3D view.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 05:17:03 -07:00
dhkatzandClaude Opus 5.5 0d126574bf Note the data types checked in the client
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 04:39:15 -07:00
dhkatzandClaude Opus 5.5 83e0665681 Declare the renderer interface's data types
Transforms, matrices, colors, lights, blend modes, fog, texture transforms, vertex arrays and the engine's buffer and texture objects, worked out from IMVU's Direct3D engine. The pass-through logs the first calls' arguments read through them, to check them in the client.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 04:36:37 -07:00
40 changed files with 3492 additions and 469 deletions

No files matched your search

+152
View File
@@ -0,0 +1,152 @@
"""Publish the renderer's programs. Run by ci.yml.
release.py version print version and channel
release.py publish <dir> publish the programs in dir: a release for a vX.Y.Z tag, else the rolling nightly
A vX.Y.Z tag is release X.Y.Z. Other builds are nightlies, "X.Y.Z-nightly.N+commit": N commits after the last
release tag (0.0.0 before the first). The nightly pre-release (tag `nightly`) is replaced on every push to main.
The same scheme as T5DE's own releases (t5de's .gitea/scripts/release.py).
"""
import hashlib
import json
import os
import re
import subprocess
import sys
import urllib.error
import urllib.request
import uuid
from pathlib import Path
ASSETS = ("t5deRenderEngine.dll", "t5de-renderer.exe")
NIGHTLY_TAG = "nightly"
USER_AGENT = "t5de-release (https://git.t5de.dev/t5de/t5de-renderer, 1)"
RELEASE_TAG = re.compile(r"v(\d+)\.(\d+)\.(\d+)")
def git(*args: str) -> str:
return subprocess.run(["git", *args], capture_output=True, text=True, check=True).stdout.strip()
def version() -> dict[str, str]:
ref_name = os.environ.get("GITHUB_REF_NAME", "")
if os.environ.get("GITHUB_REF_TYPE") == "tag" and RELEASE_TAG.fullmatch(ref_name):
return {"version": ref_name[1:], "channel": "release", "tag": ref_name}
try:
last = git("describe", "--tags", "--abbrev=0", "--match", "v[0-9]*.[0-9]*.[0-9]*")
count = int(git("rev-list", "--count", f"{last}..HEAD"))
base = last[1:]
except subprocess.CalledProcessError: # no release yet
count, base = int(git("rev-list", "--count", "HEAD")), "0.0.0"
commit = git("rev-parse", "--short=7", "HEAD")
return {"version": f"{base}-nightly.{count}+{commit}", "channel": "nightly", "tag": NIGHTLY_TAG}
class Gitea:
def __init__(self):
self.api = f"{os.environ['GITHUB_SERVER_URL']}/api/v1/repos/{os.environ['GITHUB_REPOSITORY']}"
self.token = os.environ["GITEA_TOKEN"]
def request(self, method: str, path: str, body: dict | None = None, data: bytes | None = None,
content_type: str = "application/json"):
if body is not None:
data = json.dumps(body).encode()
req = urllib.request.Request(self.api + path, method=method, data=data,
# Cloudflare in front of the server refuses Python's default User-Agent.
headers={"Authorization": f"token {self.token}", "Content-Type": content_type,
"User-Agent": USER_AGENT})
try:
with urllib.request.urlopen(req, timeout=120) as response:
text = response.read()
return json.loads(text) if text else None
except urllib.error.HTTPError as e:
if e.code == 404 and method in ("GET", "DELETE"):
return None
raise SystemExit(f"{method} {path}: {e.code} {e.read().decode(errors='replace')}")
def upload(self, release_id: int, name: str, content: bytes):
boundary = uuid.uuid4().hex
data = (f"--{boundary}\r\nContent-Disposition: form-data; name=\"attachment\"; filename=\"{name}\"\r\n"
f"Content-Type: application/octet-stream\r\n\r\n").encode() + content + f"\r\n--{boundary}--\r\n".encode()
return self.request("POST", f"/releases/{release_id}/assets?name={name}", data=data,
content_type=f"multipart/form-data; boundary={boundary}")
def notes(info: dict[str, str], files: dict[str, bytes]) -> str:
nightly = ("A nightly build of the latest commit, for testing; it's replaced by the next one.\n\n"
if info["channel"] == "nightly" else "")
hashes = "\n".join(f"`{name}` sha256: `{hashlib.sha256(content).hexdigest()}`" for name, content in files.items())
return f"""{nightly}T5DE's renderer for IMVU Classic: copy both files into a T5DE install's folder, and choose OpenGL
in the client's Settings > Advanced (with T5DE's renderer patch).
Unofficial: not made by or affiliated with IMVU. Use at your own risk.
Version {info['version']} · commit {os.environ.get('GITHUB_SHA', git('rev-parse', 'HEAD'))}
{hashes}
"""
def publish(folder: str):
info = version()
files = {name: (Path(folder) / name).read_bytes() for name in ASSETS}
gitea = Gitea()
previous = None
if info["channel"] == "nightly":
old = gitea.request("GET", f"/releases/tags/{NIGHTLY_TAG}")
if old:
previous = old["target_commitish"]
gitea.request("DELETE", f"/releases/{old['id']}")
gitea.request("DELETE", f"/tags/{NIGHTLY_TAG}")
release = gitea.request("POST", "/releases", {
"tag_name": NIGHTLY_TAG, "target_commitish": os.environ["GITHUB_SHA"],
"name": f"Nightly {info['version']}", "body": notes(info, files), "prerelease": True})
else:
release = gitea.request("GET", f"/releases/tags/{info['tag']}")
fields = {"name": f"T5DE Renderer {info['version']}", "body": notes(info, files), "prerelease": False}
if release: # a rerun: refresh it
release = gitea.request("PATCH", f"/releases/{release['id']}", fields)
for asset in release.get("assets") or []:
if asset["name"] in files:
gitea.request("DELETE", f"/releases/{release['id']}/assets/{asset['id']}")
else:
release = gitea.request("POST", "/releases", {"tag_name": info["tag"], **fields})
for name, content in files.items():
gitea.upload(release["id"], name, content)
print(f"Published {release['html_url']}")
if info["channel"] == "nightly" and os.environ.get("DISCORD_NIGHTLY_WEBHOOK"):
announce(os.environ["DISCORD_NIGHTLY_WEBHOOK"], info, release, previous)
def announce(webhook: str, info: dict[str, str], release: dict, previous: str | None):
"""Post the release to a Discord channel. Failing to doesn't fail the release."""
commits = []
if previous:
try:
commits = git("log", "--format=%h %s", "--no-merges", f"{previous}..HEAD").splitlines()
except subprocess.CalledProcessError: # the previous nightly's commit is gone (history rewritten)
pass
shown = commits[:10] + ([f"and {len(commits) - 10} more"] if len(commits) > 10 else [])
description = f"[Download]({release['html_url']})"
if shown:
description += "\n\n" + "\n".join(f"• {line}" for line in shown)
body = {"embeds": [{"title": f"Renderer Nightly {info['version']}", "url": release["html_url"],
"description": description[:4000], "color": 0xFFD200}]}
req = urllib.request.Request(webhook, method="POST", data=json.dumps(body).encode(),
headers={"Content-Type": "application/json",
"User-Agent": USER_AGENT})
try:
urllib.request.urlopen(req, timeout=30).close()
print("Announced on Discord")
except (urllib.error.URLError, TimeoutError) as e:
print(f"warning: couldn't post to Discord: {e}")
if __name__ == "__main__":
if sys.argv[1:] == ["version"]:
for key, value in version().items():
print(f"{key}={value}")
elif len(sys.argv) == 3 and sys.argv[1] == "publish":
publish(sys.argv[2])
else:
raise SystemExit(__doc__)
+59
View File
@@ -0,0 +1,59 @@
# Builds the adapter (32-bit) and the renderer process (64-bit), runs the tests, and publishes both programs: pushes to
# main replace the nightly pre-release, vX.Y.Z tags make release X.Y.Z (.gitea/scripts/release.py).
#
# Runs on T5DE's Windows runner (label `windows`, jobs run directly on the machine: it needs Visual Studio's
# compiler). The checkout lives in a fixed folder there ($T5DE_BUILD_ROOT, set in the runner's config), so Diligent
# Engine is fetched and built once, not on every run. Never on pull requests: their code would run on that machine.
name: CI
on:
push:
branches: [main]
tags: ["v*"]
workflow_dispatch:
jobs:
build:
runs-on: windows
defaults:
run:
# Steps run as script files, which Windows' default execution policy refuses.
shell: powershell -NoProfile -ExecutionPolicy Bypass -Command ". '{0}'"
steps:
- name: Check out
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
function git { & git.exe -c "http.extraHeader=Authorization: token $env:TOKEN" @args; if ($LASTEXITCODE) { exit $LASTEXITCODE } }
$root = if ($env:T5DE_BUILD_ROOT) { $env:T5DE_BUILD_ROOT } else { $env:GITHUB_WORKSPACE }
$src = Join-Path $root ($env:GITHUB_REPOSITORY -replace '/', '-')
if (-not (Test-Path (Join-Path $src .git))) { git init -q $src }
Set-Location $src
git fetch -q --force --tags "$env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY" "+${env:GITHUB_REF}:refs/remotes/ci/head"
git checkout -q --force --detach $env:GITHUB_SHA
git clean -q -ffdx -e /build/
"T5DE_SRC=$src" | Out-File -Append -Encoding utf8 $env:GITHUB_ENV
- name: Build and test the adapter
working-directory: ${{ env.T5DE_SRC }}
run: cmake --workflow --preset adapter
- name: Build the renderer process
working-directory: ${{ env.T5DE_SRC }}
run: cmake --workflow --preset renderer
- name: Collect the programs
working-directory: ${{ env.T5DE_SRC }}
run: |
cmake --install build/windows-x86 --config Release --prefix dist
if ($LASTEXITCODE) { exit $LASTEXITCODE }
cmake --install build/windows-x64 --config Release --prefix dist
- name: Publish the release
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
working-directory: ${{ env.T5DE_SRC }}
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Optional: a Discord webhook URL that nightlies are announced on.
DISCORD_NIGHTLY_WEBHOOK: ${{ secrets.DISCORD_NIGHTLY_WEBHOOK }}
run: python .gitea/scripts/release.py publish dist
+10 -5
View File
@@ -1,7 +1,9 @@
# t5de-renderer # t5de-renderer
A renderer plugin for IMVU Classic (see README.md and docs/interface.md). Part of T5DE; the patches and the patcher A new renderer for IMVU Classic: a 32-bit adapter DLL the client loads as a renderer plugin, and a 64-bit renderer
are in the `t5de` repository (git.t5de.dev/t5de/t5de, usually checked out beside this one). 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 ## Rules
@@ -13,9 +15,12 @@ are in the `t5de` repository (git.t5de.dev/t5de/t5de, usually checked out beside
## Notes ## Notes
- The plugin must match IMVU's ABI: 32-bit, MSVC (cl or clang-cl; MinGW's C++ ABI differs), `__thiscall` - 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, Boost 1.61's `shared_ptr` layout. Exceptions must not cross into IMVU's virtual methods in IMVU's vtable order (never overloaded: MSVC orders overloads unlike their declaration), Boost
code (a different C runtime); the CRT is linked statically. 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 - 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 `%LOCALAPPDATA%\t5de-dev\ghidra`, analysed headless (`support\analyzeHeadless.bat`). The client to analyse is the
`t5de` checkout's `.cache/<version>/install`. `t5de` checkout's `.cache/<version>/install`.
+27 -24
View File
@@ -5,15 +5,18 @@ project(t5de-renderer
DESCRIPTION "A new 3D renderer for the IMVU Classic client" DESCRIPTION "A new 3D renderer for the IMVU Classic client"
LANGUAGES CXX) 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 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.") message(FATAL_ERROR "Build with MSVC or clang-cl: the adapter 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.")
endif() 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>") set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
# Warnings and conformance for our own targets. # 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_options(t5de_options INTERFACE /W4 /permissive- /utf-8 /Zc:__cplusplus)
target_compile_definitions(t5de_options INTERFACE WIN32_LEAN_AND_MEAN NOMINMAX UNICODE _UNICODE) target_compile_definitions(t5de_options INTERFACE WIN32_LEAN_AND_MEAN NOMINMAX UNICODE _UNICODE)
# Our declarations of IMVU's interfaces. # What the adapter and the renderer process share.
add_library(t5de_imvu INTERFACE) add_library(t5de_protocol INTERFACE)
add_library(t5de::imvu ALIAS t5de_imvu) add_library(t5de::protocol ALIAS t5de_protocol)
target_include_directories(t5de_imvu INTERFACE target_include_directories(t5de_protocol INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>) target_compile_features(t5de_protocol INTERFACE cxx_std_20)
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 .)
include(CTest) 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) add_subdirectory(tests)
endif()
else()
add_subdirectory(renderer)
endif() endif()
+33 -12
View File
@@ -3,29 +3,43 @@
"cmakeMinimumRequired": { "major": 3, "minor": 28, "patch": 0 }, "cmakeMinimumRequired": { "major": 3, "minor": 28, "patch": 0 },
"configurePresets": [ "configurePresets": [
{ {
"name": "windows-x86", "name": "windows",
"displayName": "Windows, 32-bit (Visual Studio Build Tools)", "hidden": true,
"description": "IMVU Classic is a 32-bit Visual C++ program: build for Win32 with MSVC.",
"generator": "Visual Studio 18 2026", "generator": "Visual Studio 18 2026",
"architecture": { "value": "Win32", "strategy": "set" },
"binaryDir": "${sourceDir}/build/${presetName}", "binaryDir": "${sourceDir}/build/${presetName}",
"installDir": "${sourceDir}/build/${presetName}/install", "installDir": "${sourceDir}/build/install",
"condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" } "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": [ "buildPresets": [
{ "name": "debug", "configurePreset": "windows-x86", "configuration": "Debug" }, { "name": "adapter-debug", "configurePreset": "windows-x86", "configuration": "Debug" },
{ "name": "release", "configurePreset": "windows-x86", "configuration": "Release" } { "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": [ "testPresets": [
{ {
"name": "debug", "name": "adapter-debug",
"configurePreset": "windows-x86", "configurePreset": "windows-x86",
"configuration": "Debug", "configuration": "Debug",
"output": { "outputOnFailure": true } "output": { "outputOnFailure": true }
}, },
{ {
"name": "release", "name": "adapter-release",
"configurePreset": "windows-x86", "configurePreset": "windows-x86",
"configuration": "Release", "configuration": "Release",
"output": { "outputOnFailure": true } "output": { "outputOnFailure": true }
@@ -33,11 +47,18 @@
], ],
"workflowPresets": [ "workflowPresets": [
{ {
"name": "release", "name": "adapter",
"steps": [ "steps": [
{ "type": "configure", "name": "windows-x86" }, { "type": "configure", "name": "windows-x86" },
{ "type": "build", "name": "release" }, { "type": "build", "name": "adapter-release" },
{ "type": "test", "name": "release" } { "type": "test", "name": "adapter-release" }
]
},
{
"name": "renderer",
"steps": [
{ "type": "configure", "name": "windows-x64" },
{ "type": "build", "name": "renderer-release" }
] ]
} }
] ]
+14 -11
View File
@@ -1,25 +1,28 @@
# t5de-renderer # t5de-renderer
A new 3D renderer for the IMVU Classic client, as a renderer plugin the client loads like its own OpenGL renderer. A new 3D renderer for the IMVU Classic client. Part of [T5DE](https://git.t5de.dev/t5de/t5de); unofficial, not
Part of [T5DE](https://git.t5de.dev/t5de/t5de); unofficial, not affiliated with IMVU. 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 Two programs (`docs/architecture.md`): an adapter, `t5deRenderEngine.dll`, which IMVU Classic loads as a renderer
declarations of IMVU's interface (`docs/interface.md`). 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 ## Building
Needs Visual Studio Build Tools 2026 (the C++ workload) and CMake 3.28 or newer. IMVU Classic is a 32-bit Needs Visual Studio Build Tools 2026 (the C++ workload) and CMake 3.28 or newer. One preset per program:
Visual C++ program, so the plugin is built for 32-bit Windows (the `windows-x86` preset):
```sh ```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: The renderer's first configure fetches Diligent Engine (pinned in `renderer/CMakeLists.txt`) and builds it, which
`cmake --preset windows-x86`, `cmake --build --preset debug`, `ctest --preset debug`. takes a while.
## Trying it ## Trying it
Copy `t5deRenderEngine.dll` into a T5DE client folder built with the `t5de-renderer` patch, which has the OpenGL Copy both programs into a T5DE client folder built with the `t5de-renderer` patch, which has the OpenGL setting load
setting load it in place of `glRenderEngine.dll`, then choose OpenGL in Settings > Advanced. The log is the adapter in place of `glRenderEngine.dll`, then choose OpenGL in Settings > Advanced. The adapter's log is
`%TEMP%\t5de-renderer.log`. `%TEMP%\t5de-renderer.log`.
+15
View File
@@ -0,0 +1,15 @@
# t5deRenderEngine.dll, the adapter: a renderer plugin for IMVU Classic (32-bit).
add_library(t5de_render_engine SHARED
engine.cpp
log.cpp
pixmap.cpp
plugin.cpp
plugin.def
recorder.cpp
renderer_process.cpp
resources.cpp)
set_target_properties(t5de_render_engine PROPERTIES OUTPUT_NAME t5deRenderEngine)
target_link_libraries(t5de_render_engine PRIVATE t5de::imvu t5de::protocol t5de::options)
install(TARGETS t5de_render_engine RUNTIME DESTINATION .)
+372
View File
@@ -0,0 +1,372 @@
#include "engine.hpp"
#include <windows.h>
#include <bit>
#include <cmath>
#include <cstring>
#include <vector>
#include "log.hpp"
#include "resources.hpp"
namespace t5de {
using protocol::Op;
using protocol::State;
namespace {
protocol::SetTransform transform_command(const imvu::Transform& t)
{
protocol::SetTransform command{};
std::memcpy(command.m, &t.linear[0][0], sizeof(float) * 9);
command.m[9] = t.translation.x;
command.m[10] = t.translation.y;
command.m[11] = t.translation.z;
return command;
}
} // namespace
Engine::Engine(imvu::SharedPtr<imvu::PiRenderEngine> inner, std::shared_ptr<Recorder> recorder)
: inner_(std::move(inner)), recorder_(std::move(recorder))
{
}
Engine::~Engine() { log::info("engine: destroyed after {} frames", frames_); }
void Engine::setState(State state, std::uint32_t value)
{
recorder_->add(Op::SetState, protocol::SetState{state, value});
}
void Engine::resizeFrameBuffer(imvu::Vec2i size) { inner_->resizeFrameBuffer(size); }
void Engine::enable2dFrameBuffer() { inner_->enable2dFrameBuffer(); }
void Engine::clear(std::uint32_t flags, imvu::Color color)
{
if (innerDraws()) {
inner_->clear(flags, color);
}
recorder_->add(Op::Clear, protocol::Clear{flags, color});
}
void Engine::setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection)
{
inner_->setProjection(viewport, projection);
if (projection.m[2][3] == 0.0f) {
++stats_.flat;
}
protocol::SetProjection command{viewport.x, viewport.y, {}};
std::memcpy(command.m, &projection.m[0][0], sizeof(command.m));
recorder_->add(Op::SetProjection, command);
}
void Engine::setModelSpace(const imvu::Transform& model)
{
inner_->setModelSpace(model);
recorder_->add(Op::SetModel, transform_command(model));
}
void Engine::setViewSpace(const imvu::Transform& view)
{
inner_->setViewSpace(view);
recorder_->add(Op::SetView, transform_command(view));
}
void Engine::setTextures(imvu::PiRenderTexture* texture0, imvu::PiRenderTexture* texture1)
{
inner_->setTextures(inner(texture0), inner(texture1));
for (imvu::PiRenderTexture* texture : {texture0, texture1}) {
if (texture != nullptr && id(texture) == 0 && foreignLogged_ < 20) {
++foreignLogged_;
log::info("engine: frame {}: a texture the client renderer made ({})", frames_, static_cast<void*>(texture));
}
}
recorder_->add(Op::SetTextures, protocol::SetTextures{id(texture0), id(texture1)});
}
void Engine::setDepthTestingOn(bool on)
{
inner_->setDepthTestingOn(on);
setState(State::DepthTest, on);
}
void Engine::setDepthWritingOn(bool on)
{
inner_->setDepthWritingOn(on);
setState(State::DepthWrite, on);
}
void Engine::setBlendMode(imvu::RenderBlendMode mode)
{
inner_->setBlendMode(mode);
setState(State::BlendMode, static_cast<std::uint32_t>(mode));
}
void Engine::setAlphaTestingOn(bool on)
{
inner_->setAlphaTestingOn(on);
setState(State::AlphaTest, on);
}
void Engine::setAlphaTestValue(float value)
{
inner_->setAlphaTestValue(value);
setState(State::AlphaReference, static_cast<std::uint32_t>(std::lround(value * 255.0f)));
}
void Engine::setBlendingOn(bool on)
{
inner_->setBlendingOn(on);
setState(State::Blending, on);
}
void Engine::setBackFaceCullingOn(bool on)
{
inner_->setBackFaceCullingOn(on);
setState(State::Culling, on);
}
void Engine::setNormalInvertingOn(bool on)
{
inner_->setNormalInvertingOn(on);
setState(State::NormalInverting, on);
}
void Engine::setLightingOn(bool on)
{
inner_->setLightingOn(on);
setState(State::Lighting, on);
}
void Engine::setLightProperties(std::uint32_t index, const imvu::LightProperties& light)
{
inner_->setLightProperties(index, light);
protocol::SetLight command{index, light.enabled, light.diffuse,
light.type == imvu::LightType::Directional,
{light.vector.x, light.vector.y, light.vector.z}};
// IMVU's Direct3D renderer shines a directional light along -vector, along -x for a zero vector.
if (command.directional && light.vector.x == 0 && light.vector.y == 0 && light.vector.z == 0) {
command.vector[0] = 1;
}
recorder_->add(Op::SetLight, command);
}
void Engine::setFogOn(bool on)
{
inner_->setFogOn(on);
setState(State::Fog, on);
}
void Engine::setFogAttributes(float start, float end, imvu::Color color)
{
inner_->setFogAttributes(start, end, color);
recorder_->add(Op::SetFog, protocol::SetFog{start, end, color});
}
void Engine::setTextureTransformMatrixIdentity(std::uint32_t stage)
{
inner_->setTextureTransformMatrixIdentity(stage);
recorder_->add(Op::SetTextureTransform, protocol::SetTextureTransform{stage, 1, {1, 0, 0, 1, 0, 0}});
}
void Engine::setTextureTransformMatrix(std::uint32_t stage, const imvu::TextureTransform& transform)
{
inner_->setTextureTransformMatrix(stage, transform);
protocol::SetTextureTransform command{stage, 0, {}};
std::memcpy(command.m, transform.m, sizeof(command.m));
recorder_->add(Op::SetTextureTransform, command);
}
void Engine::setEnvironmentAmbient(imvu::Color color)
{
inner_->setEnvironmentAmbient(color);
setState(State::Ambient, color);
}
imvu::PiRenderTexture* Engine::newRenderTexture(const imvu::Pixmap& pixmap, std::uint32_t flags)
{
imvu::PiRenderTexture* texture = inner_->newRenderTexture(pixmap, flags);
if (texture == nullptr) {
return nullptr;
}
return new Texture(texture, recorder_, recorder_->createTexture(pixmap, flags));
}
imvu::PiInternalIndexBuffer* Engine::newIndexBuffer(std::uint32_t count, const std::uint16_t* indices)
{
imvu::PiInternalIndexBuffer* buffer = inner_->newIndexBuffer(count, indices);
if (buffer == nullptr) {
return nullptr;
}
return new IndexBuffer(buffer, recorder_,
recorder_->createBuffer(protocol::BufferKind::Index, indices, 2, 2, count));
}
imvu::PiInternalVertexBuffer* Engine::newStaticVertexBuffer(const void* data, std::uint32_t stride,
std::uint32_t elementSize, std::uint32_t elementCount)
{
imvu::PiInternalVertexBuffer* buffer = inner_->newStaticVertexBuffer(data, stride, elementSize, elementCount);
if (buffer == nullptr) {
return nullptr;
}
return new VertexBuffer(
buffer, recorder_,
recorder_->createBuffer(protocol::BufferKind::Vertex, data, stride, elementSize, elementCount));
}
imvu::PiInternalVertexBuffer* Engine::newDynamicVertexBuffer(std::uint32_t elementSize, std::uint32_t elementCount)
{
imvu::PiInternalVertexBuffer* buffer = inner_->newDynamicVertexBuffer(elementSize, elementCount);
if (buffer == nullptr) {
return nullptr;
}
return new VertexBuffer(
buffer, recorder_,
recorder_->createBuffer(protocol::BufferKind::Vertex, nullptr, 0, elementSize, elementCount));
}
imvu::SharedPtr<imvu::PiPixmapRenderTarget> Engine::newPixmapRenderTarget(imvu::Vec2i size)
{
log::info("engine: frame {}: a {}x{} pixmap render target", frames_, size.x, size.y);
return inner_->newPixmapRenderTarget(size);
}
imvu::SharedPtr<imvu::PiTextureRenderTarget> Engine::newTextureRenderTarget(imvu::Vec2i size)
{
log::info("engine: frame {}: a {}x{} texture render target", frames_, size.x, size.y);
return inner_->newTextureRenderTarget(size);
}
void Engine::beginRendering(imvu::PiRenderTarget* target)
{
inner_->beginRendering(target);
// Decided per frame: frames into render targets, and frames while the renderer process doesn't cover the view, are
// drawn by the client's renderer.
const bool takenOver = target == nullptr && recorder_->covering();
if (takenOver != takenOver_ && target == nullptr) {
log::info("engine: frame {}: {}", frames_, takenOver ? "drawn by the renderer process" : "drawn by the client's renderer");
}
takenOver_ = takenOver;
if (target != nullptr && foreignLogged_ < 20) {
++foreignLogged_;
log::info("engine: frame {}: drawing into a render target ({})", frames_, static_cast<void*>(target));
}
recorder_->add(Op::BeginFrame, protocol::BeginFrame{target != nullptr});
}
void Engine::endRendering()
{
inner_->endRendering();
LARGE_INTEGER now;
QueryPerformanceCounter(&now);
recorder_->add(Op::EndFrame, protocol::EndFrame{static_cast<std::uint64_t>(now.QuadPart)});
recorder_->flush(true);
++frames_;
if (frames_ % 300 == 0 && statsLogged_ < 6) {
++statsLogged_;
log::info("engine: last 300 frames: drawTriangles {} (and {} with buffers not ours), drawTrianglesFromMemory {} "
"(and {} without indices), point sprites {}, lines {}, 2D projections {}",
stats_.triangles, stats_.foreignTriangles, stats_.indexed, stats_.unindexed, stats_.sprites, stats_.lines,
stats_.flat);
stats_ = {};
}
}
void Engine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
imvu::PiInternalVertexBuffer* colors)
{
if (innerDraws()) {
inner_->drawTriangles(inner(indices), inner(positions), inner(normals), inner(texCoords), inner(colors));
}
++(id(indices) != 0 && id(positions) != 0 ? stats_.triangles : stats_.foreignTriangles);
recorder_->add(Op::DrawTriangles,
protocol::DrawTriangles{id(indices), id(positions), id(normals), id(texCoords), id(colors)});
}
void Engine::drawLine(imvu::Vec3f from, imvu::Vec3f to, imvu::Color color)
{
if (innerDraws()) {
inner_->drawLine(from, to, color);
}
++stats_.lines;
}
void Engine::drawPointSprites(const imvu::PointSpriteVector& sprites)
{
if (innerDraws()) {
inner_->drawPointSprites(sprites);
}
++stats_.sprites;
if (sprites.first == nullptr || sprites.last <= sprites.first) {
return;
}
const auto count = static_cast<std::uint32_t>(sprites.last - sprites.first);
if (std::byte* payload = recorder_->add(Op::DrawPointSprites, protocol::DrawPointSprites{count},
std::size_t{count} * sizeof(imvu::PointSprite))) {
std::memcpy(payload, sprites.first, std::size_t{count} * sizeof(imvu::PointSprite));
}
}
void Engine::drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount,
const std::uint16_t* indices,
const imvu::VertexAttributeArray<imvu::Vec3f>& positions,
const imvu::VertexAttributeArray<imvu::Vec3f>& normals,
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
const imvu::VertexAttributeArray<imvu::Color>& colors)
{
if (innerDraws()) {
inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, colors);
}
++(indices != nullptr ? stats_.indexed : stats_.unindexed);
if (positions.data == nullptr || vertexCount == 0) {
return;
}
// Without indices: the vertices in order, as a triangle list.
std::vector<std::uint16_t> sequence;
if (indices == nullptr) {
if (vertexCount > 0xffff) {
return;
}
sequence.resize(vertexCount - vertexCount % 3);
for (std::size_t i = 0; i < sequence.size(); ++i) {
sequence[i] = static_cast<std::uint16_t>(i);
}
indices = sequence.data();
indexCount = static_cast<std::uint32_t>(sequence.size());
}
const bool hasNormals = normals.data != nullptr && normals.count >= vertexCount;
const bool hasTexCoords = texCoords.data != nullptr && texCoords.count >= vertexCount;
const bool hasColors = colors.data != nullptr && colors.count >= vertexCount;
const std::size_t indexBytes = protocol::padded(std::size_t{indexCount} * 2);
const std::size_t size = indexBytes + std::size_t{vertexCount} * (12 + (hasNormals ? 12 : 0) +
(hasTexCoords ? 8 : 0) + (hasColors ? 4 : 0));
std::byte* payload = recorder_->add(Op::DrawArrays,
protocol::DrawArrays{vertexCount, indexCount, hasNormals, hasTexCoords,
hasColors},
size);
if (payload == nullptr) {
return;
}
std::memcpy(payload, indices, std::size_t{indexCount} * 2);
std::byte* next = payload + indexBytes;
pack(next, positions.data, positions.stride, 12, vertexCount);
next += std::size_t{vertexCount} * 12;
if (hasNormals) {
pack(next, normals.data, normals.stride, 12, vertexCount);
next += std::size_t{vertexCount} * 12;
}
if (hasTexCoords) {
pack(next, texCoords.data, texCoords.stride, 8, vertexCount);
next += std::size_t{vertexCount} * 8;
}
if (hasColors) {
pack(next, colors.data, colors.stride, 4, vertexCount);
}
}
} // namespace t5de
+93
View File
@@ -0,0 +1,93 @@
#pragma once
// The adapter's engine: what the client draws with. Everything is recorded for the renderer process, which draws the
// 3D view. The client's own Direct3D renderer gets everything too (state and resources: it still draws into render
// targets and reads textures back, which the renderer process doesn't yet), but while the renderer process covers the
// whole view, the client's renderer skips its frames' clears and draws. When the renderer process doesn't cover it
// (it failed, or shows only part of the view, for comparing) the client's renderer draws everything, from the next
// frame on.
#include <cstdint>
#include <memory>
#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).
Engine(imvu::SharedPtr<imvu::PiRenderEngine> inner, std::shared_ptr<Recorder> recorder);
~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_;
std::uint64_t frames_ = 0;
bool takenOver_ = false; // this frame is drawn by the renderer process only
int foreignLogged_ = 0; // diagnostics: textures the client renderer made, render targets
// Diagnostics: what the client draws, summed over 300 frames and logged (the first few times).
struct Stats {
std::uint32_t triangles = 0; // drawTriangles with our buffers
std::uint32_t foreignTriangles = 0; // drawTriangles with buffers that aren't ours (not drawn)
std::uint32_t indexed = 0; // drawTrianglesFromMemory with indices
std::uint32_t unindexed = 0; // drawTrianglesFromMemory without
std::uint32_t sprites = 0;
std::uint32_t lines = 0;
std::uint32_t flat = 0; // setProjection with an orthographic projection (2D)
};
Stats stats_;
int statsLogged_ = 0;
};
} // namespace t5de
View File
File renamed without changes.
View File
File renamed without changes.
+45
View File
@@ -0,0 +1,45 @@
#include "pixmap.hpp"
#include <windows.h>
namespace t5de {
namespace {
using Getter = unsigned(__thiscall*)(const imvu::Pixmap*);
using PixelsGetter = std::uint32_t*(__thiscall*)(const imvu::Pixmap*);
struct Methods {
Getter width = nullptr;
Getter height = nullptr;
Getter rowBytes = nullptr;
PixelsGetter pixels = nullptr;
};
const Methods& methods()
{
static const Methods methods = [] {
Methods found;
if (HMODULE module = GetModuleHandleW(L"pixmap.dll")) {
found.width = reinterpret_cast<Getter>(GetProcAddress(module, "?width@Pixmap@@QBEIXZ"));
found.height = reinterpret_cast<Getter>(GetProcAddress(module, "?height@Pixmap@@QBEIXZ"));
found.rowBytes = reinterpret_cast<Getter>(GetProcAddress(module, "?rowBytes@Pixmap@@QBEIXZ"));
found.pixels = reinterpret_cast<PixelsGetter>(GetProcAddress(module, "?pixels@Pixmap@@QBEPAIXZ"));
}
return found;
}();
return methods;
}
} // namespace
std::optional<PixmapPixels> pixmap_pixels(const imvu::Pixmap& pixmap)
{
const Methods& m = methods();
if (m.width == nullptr || m.height == nullptr || m.rowBytes == nullptr || m.pixels == nullptr) {
return std::nullopt;
}
return PixmapPixels{m.width(&pixmap), m.height(&pixmap), m.rowBytes(&pixmap), m.pixels(&pixmap)};
}
} // namespace t5de
+21
View File
@@ -0,0 +1,21 @@
#pragma once
// Reading IMVU's images (Pixmap) through pixmap.dll's exported methods, rather than depending on their layout.
#include <cstdint>
#include <optional>
#include "t5de/imvu/types.hpp"
namespace t5de {
struct PixmapPixels {
std::uint32_t width;
std::uint32_t height;
std::uint32_t rowBytes;
const std::uint32_t* pixels; // 0xAARRGGBB, rows rowBytes apart
};
// Null if pixmap.dll's methods can't be found.
std::optional<PixmapPixels> pixmap_pixels(const imvu::Pixmap& pixmap);
} // namespace t5de
+24 -3
View File
@@ -1,12 +1,16 @@
// The renderer plugin's entry points, which SceneWindow.dll's createDLLRenderer looks up by name (exported // 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 <windows.h>
#include <exception> #include <exception>
#include <string_view>
#include "log.hpp" #include "log.hpp"
#include "passthrough_engine.hpp" #include "engine.hpp"
#include "recorder.hpp"
#include "renderer_process.hpp"
#include "t5de/imvu/render_engine.hpp" #include "t5de/imvu/render_engine.hpp"
namespace { namespace {
@@ -29,6 +33,19 @@ imvu::SharedPtr<imvu::PiRenderEngine> create_imvu_engine(imvu::SharedPtr<imvu::I
return create(std::move(window), imvu::RendererType::Direct3D); return create(std::move(window), imvu::RendererType::Direct3D);
} }
// What the renderer process draws: T5DE_RENDERER_VIEW=off, split (the 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 } // namespace
extern "C" int __cdecl isRenderEngineSupported() { return 1; } extern "C" int __cdecl isRenderEngineSupported() { return 1; }
@@ -39,11 +56,15 @@ extern "C" imvu::SharedPtr<imvu::PiRenderEngine> __cdecl createRenderEngine(imvu
{ {
try { try {
log::info("createRenderEngine: window {}", static_cast<void*>(window.get())); log::info("createRenderEngine: window {}", static_cast<void*>(window.get()));
HWND hwnd = window ? window->getPlatformWindow() : nullptr;
auto inner = create_imvu_engine(std::move(window)); auto inner = create_imvu_engine(std::move(window));
if (!inner) { if (!inner) {
return {}; 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))));
} catch (const std::exception& e) { } catch (const std::exception& e) {
// Exceptions mustn't cross into IMVU's code (a different C++ runtime). // Exceptions mustn't cross into IMVU's code (a different C++ runtime).
log::info("createRenderEngine failed: {}", e.what()); log::info("createRenderEngine failed: {}", e.what());
File renamed without changes.
+111
View File
@@ -0,0 +1,111 @@
#include "recorder.hpp"
#include <cstring>
#include "log.hpp"
#include "pixmap.hpp"
namespace t5de {
namespace {
// Between frames, recorded commands (mostly textures and buffers being loaded) are sent once they reach this size.
constexpr std::size_t flushSize = 4 * 1024 * 1024;
} // namespace
void pack(std::byte* destination, const void* source, std::uint32_t stride, std::uint32_t elementSize,
std::uint32_t count)
{
const auto* from = static_cast<const std::byte*>(source);
if (stride == 0 || stride == elementSize) {
std::memcpy(destination, from, std::size_t{elementSize} * count);
return;
}
for (std::uint32_t i = 0; i < count; ++i) {
std::memcpy(destination + std::size_t{i} * elementSize, from + std::size_t{i} * stride, elementSize);
}
}
Recorder::Recorder(std::unique_ptr<RendererProcess> process) : process_(std::move(process)) {}
void Recorder::flush(bool endsFrame)
{
if (!active()) {
writer_.clear();
return;
}
process_->send(writer_.bytes(), endsFrame);
writer_.clear();
}
void Recorder::flushIfLarge()
{
if (writer_.size() >= flushSize) {
flush(false);
}
}
void Recorder::addPixels(protocol::Op op, std::uint32_t id, const imvu::Pixmap& pixmap, std::uint32_t flags)
{
const auto pixels = pixmap_pixels(pixmap);
if (!pixels || pixels->pixels == nullptr) {
return;
}
const std::size_t rowSize = std::size_t{pixels->width} * 4;
std::byte* payload = nullptr;
if (op == protocol::Op::CreateTexture) {
payload = add(op, protocol::CreateTexture{id, pixels->width, pixels->height, flags}, rowSize * pixels->height);
} else {
payload = add(op, protocol::UpdateTexture{id, pixels->width, pixels->height}, rowSize * pixels->height);
}
if (payload == nullptr) {
return;
}
const auto* rows = reinterpret_cast<const std::byte*>(pixels->pixels);
for (std::uint32_t y = 0; y < pixels->height; ++y) {
std::memcpy(payload + y * rowSize, rows + std::size_t{y} * pixels->rowBytes, rowSize);
}
flushIfLarge();
}
std::uint32_t Recorder::createTexture(const imvu::Pixmap& pixmap, std::uint32_t flags)
{
const std::uint32_t id = nextId_++;
addPixels(protocol::Op::CreateTexture, id, pixmap, flags);
return id;
}
void Recorder::updateTexture(std::uint32_t id, const imvu::Pixmap& pixmap)
{
addPixels(protocol::Op::UpdateTexture, id, pixmap, 0);
}
void Recorder::destroyTexture(std::uint32_t id) { add(protocol::Op::DestroyTexture, protocol::DestroyTexture{id}); }
std::uint32_t Recorder::createBuffer(protocol::BufferKind kind, const void* data, std::uint32_t stride,
std::uint32_t elementSize, std::uint32_t count)
{
const std::uint32_t id = nextId_++;
const std::size_t size = data != nullptr ? std::size_t{elementSize} * count : 0;
if (std::byte* payload = add(protocol::Op::CreateBuffer, protocol::CreateBuffer{id, kind, elementSize, count}, size);
payload != nullptr && size != 0) {
pack(payload, data, stride, elementSize, count);
}
flushIfLarge();
return id;
}
void Recorder::updateBuffer(std::uint32_t id, const void* data, std::uint32_t stride, std::uint32_t elementSize,
std::uint32_t count)
{
const std::size_t size = std::size_t{elementSize} * count;
if (std::byte* payload = add(protocol::Op::UpdateBuffer, protocol::UpdateBuffer{id, elementSize, count}, size);
payload != nullptr && data != nullptr) {
pack(payload, data, stride, elementSize, count);
}
}
void Recorder::destroyBuffer(std::uint32_t id) { add(protocol::Op::DestroyBuffer, protocol::DestroyBuffer{id}); }
} // namespace t5de
+62
View File
@@ -0,0 +1,62 @@
#pragma once
// Records what the client draws as commands (include/t5de/protocol/commands.hpp) and sends them to the renderer
// process. Shared by an engine and the resources it creates (which can outlive it); once the renderer process is
// gone, it records nothing.
#include <cstdint>
#include <memory>
#include "renderer_process.hpp"
#include "t5de/imvu/types.hpp"
#include "t5de/protocol/commands.hpp"
namespace t5de {
class Recorder {
public:
explicit Recorder(std::unique_ptr<RendererProcess> process);
[[nodiscard]] bool active() const { return process_ && process_->alive(); }
// Whether the renderer process draws the whole 3D view, so the client's renderer needn't.
[[nodiscard]] bool covering() const { return process_ && process_->covering(); }
// Records a command (if active); returns its payload to fill, or null.
template <class Body>
std::byte* add(protocol::Op op, const Body& body, std::size_t payloadSize = 0)
{
if (!active()) {
return nullptr;
}
std::byte* payload = writer_.add(op, body, payloadSize);
return payload;
}
// Sends what's recorded: at the end of each frame, and between frames when a lot is waiting (resources).
void flush(bool endsFrame);
void flushIfLarge();
std::uint32_t createTexture(const imvu::Pixmap& pixmap, std::uint32_t flags);
void updateTexture(std::uint32_t id, const imvu::Pixmap& pixmap);
void destroyTexture(std::uint32_t id);
// Copies count elements of elementSize bytes, stride bytes apart (0: packed), from data (null: none yet).
std::uint32_t createBuffer(protocol::BufferKind kind, const void* data, std::uint32_t stride,
std::uint32_t elementSize, std::uint32_t count);
void updateBuffer(std::uint32_t id, const void* data, std::uint32_t stride, std::uint32_t elementSize,
std::uint32_t count);
void destroyBuffer(std::uint32_t id);
private:
void addPixels(protocol::Op op, std::uint32_t id, const imvu::Pixmap& pixmap, std::uint32_t flags);
std::unique_ptr<RendererProcess> process_;
protocol::Writer writer_;
std::uint32_t nextId_ = 1;
};
// Copies count elements of elementSize bytes, stride bytes apart (0: packed), into destination, packed.
void pack(std::byte* destination, const void* source, std::uint32_t stride, std::uint32_t elementSize,
std::uint32_t count);
} // namespace t5de
+153
View File
@@ -0,0 +1,153 @@
#include "renderer_process.hpp"
#include <algorithm>
#include <atomic>
#include <cstring>
#include <filesystem>
#include <format>
#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;
}
} // namespace
std::unique_ptr<RendererProcess> RendererProcess::start(HWND window, protocol::View view)
{
static std::atomic<int> engines = 0;
const std::wstring name = std::format(L"Local\\t5de-renderer-{}-{}", GetCurrentProcessId(), engines++);
constexpr DWORD size = sizeof(protocol::Channel) + protocol::channelCapacity;
std::unique_ptr<RendererProcess> renderer(new RendererProcess());
renderer->mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, 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());
if (renderer->channel_ == nullptr || renderer->ready_ == nullptr || renderer->consumed_ == nullptr) {
log::info("renderer process: can't set up the channel: error {}", GetLastError());
return nullptr;
}
renderer->data_ = reinterpret_cast<std::byte*>(renderer->channel_ + 1);
*renderer->channel_ = {.version = protocol::version,
.closing = 0,
.window = reinterpret_cast<std::uintptr_t>(window),
.frames = 0,
.view = view,
.capacity = protocol::channelCapacity,
.chunkSize = 0,
.chunkFlags = 0,
.covering = 0,
.reserved = 0};
// The renderer's window goes on top of the client's 3D view, as a child window. The client's window mustn't clip
// its children (WS_CLIPCHILDREN, which the client creates it with): the client draws a frame when its window gets
// WM_PAINT, which a window entirely covered by clipped children never gets (it then drew about once a second). The
// renderer's swap chain is a flip model one, which the compositor shows above the client's drawing anyway.
SetWindowLongW(window, GWL_STYLE, GetWindowLongW(window, GWL_STYLE) & ~WS_CLIPCHILDREN);
const auto exe = running_copy(module_folder() / L"t5de-renderer.exe");
std::wstring command = std::format(L"\"{}\" --channel {} --parent {}", exe.wstring(), name, GetCurrentProcessId());
STARTUPINFOW startup{.cb = sizeof(STARTUPINFOW)};
PROCESS_INFORMATION process{};
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 nullptr;
}
CloseHandle(process.hThread);
renderer->process_ = process.hProcess;
log::info("renderer process: started {} (pid {}) on channel {}", exe.string(), process.dwProcessId, narrow(name));
return renderer;
}
RendererProcess::~RendererProcess()
{
if (channel_ != nullptr) {
channel_->closing = 1;
if (ready_ != nullptr) {
SetEvent(ready_);
}
UnmapViewOfFile(channel_);
}
for (HANDLE handle : {ready_, consumed_, 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;
}
} // namespace t5de
+45
View File
@@ -0,0 +1,45 @@
#pragma once
// The renderer process (t5de-renderer.exe, 64-bit) an engine draws through: started with the engine, sent the
// command stream through a shared memory channel (include/t5de/protocol/channel.hpp), and told to exit with it.
#include <windows.h>
#include <cstddef>
#include <memory>
#include <span>
#include "t5de/protocol/channel.hpp"
namespace t5de {
class RendererProcess {
public:
// Starts t5de-renderer.exe from this DLL's folder, drawing into part of window; null if it can't.
static std::unique_ptr<RendererProcess> start(HWND window, protocol::View view);
RendererProcess(const RendererProcess&) = delete;
RendererProcess& operator=(const RendererProcess&) = delete;
~RendererProcess();
// Sends commands, in as many chunks as they take; endsFrame marks the end of a frame. False if the renderer
// process is gone or stopped taking them (it's then given up on).
bool send(std::span<const std::byte> commands, bool endsFrame);
[[nodiscard]] bool alive() const { return alive_; }
// Whether the renderer process covers the whole 3D view (protocol::Channel::covering).
[[nodiscard]] bool covering() const { return alive_ && channel_->covering != 0; }
private:
RendererProcess() = default;
HANDLE mapping_ = nullptr;
HANDLE ready_ = nullptr;
HANDLE consumed_ = nullptr;
HANDLE process_ = nullptr;
protocol::Channel* channel_ = nullptr;
std::byte* data_ = nullptr;
bool alive_ = true;
};
} // namespace t5de
+108
View File
@@ -0,0 +1,108 @@
#include "resources.hpp"
#include <mutex>
#include <unordered_set>
namespace t5de {
namespace {
// Our live resources, by address.
struct Registry {
std::mutex mutex;
std::unordered_set<const void*> objects;
};
Registry& registry()
{
static Registry instance;
return instance;
}
void add(const void* object)
{
const std::scoped_lock lock(registry().mutex);
registry().objects.insert(object);
}
void remove(const void* object)
{
const std::scoped_lock lock(registry().mutex);
registry().objects.erase(object);
}
} // namespace
bool ours(const void* object)
{
if (object == nullptr) {
return false;
}
const std::scoped_lock lock(registry().mutex);
return registry().objects.contains(object);
}
Texture::Texture(imvu::PiRenderTexture* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
: inner_(inner), recorder_(std::move(recorder)), id_(id)
{
add(static_cast<imvu::PiRenderTexture*>(this));
}
Texture::~Texture()
{
remove(static_cast<imvu::PiRenderTexture*>(this));
delete inner_; // through its virtual destructor: IMVU's code frees it
recorder_->destroyTexture(id_);
}
imvu::Vec2i Texture::size() const { return inner_->size(); }
void Texture::updateRegions(const imvu::Pixmap& pixmap, const imvu::CompoundRegion& regions)
{
inner_->updateRegions(pixmap, regions);
recorder_->updateTexture(id_, pixmap); // all of it: the regions' layout isn't known
}
void Texture::update(const imvu::Pixmap& pixmap)
{
inner_->update(pixmap);
recorder_->updateTexture(id_, pixmap);
}
imvu::Pixmap* Texture::read(imvu::Pixmap* result) const { return inner_->read(result); }
IndexBuffer::IndexBuffer(imvu::PiInternalIndexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
: inner_(inner), recorder_(std::move(recorder)), id_(id)
{
add(static_cast<imvu::PiInternalIndexBuffer*>(this));
}
IndexBuffer::~IndexBuffer()
{
remove(static_cast<imvu::PiInternalIndexBuffer*>(this));
delete inner_;
recorder_->destroyBuffer(id_);
}
VertexBuffer::VertexBuffer(imvu::PiInternalVertexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
: inner_(inner), recorder_(std::move(recorder)), id_(id)
{
add(static_cast<imvu::PiInternalVertexBuffer*>(this));
}
VertexBuffer::~VertexBuffer()
{
remove(static_cast<imvu::PiInternalVertexBuffer*>(this));
delete inner_;
recorder_->destroyBuffer(id_);
}
void VertexBuffer::update(const void* data, std::uint32_t stride, std::uint32_t elementSize, std::uint32_t elementCount)
{
inner_->update(data, stride, elementSize, elementCount);
recorder_->updateBuffer(id_, data, stride, elementSize, elementCount);
}
void VertexBuffer::unknown2() { inner_->unknown2(); }
} // namespace t5de
+95
View File
@@ -0,0 +1,95 @@
#pragma once
// The textures and buffers the adapter's engine hands to the client: each wraps the client renderer's own (which the
// client renderer still draws with) and mirrors it, by id, to the renderer process. The client deletes them through
// their virtual destructors.
#include <cstdint>
#include <memory>
#include "recorder.hpp"
#include "t5de/imvu/resources.hpp"
namespace t5de {
class Texture final : public imvu::PiRenderTexture {
public:
Texture(imvu::PiRenderTexture* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id);
~Texture() override;
imvu::Vec2i size() const override;
void updateRegions(const imvu::Pixmap& pixmap, const imvu::CompoundRegion& regions) override;
void update(const imvu::Pixmap& pixmap) override;
imvu::Pixmap* read(imvu::Pixmap* result) const override;
[[nodiscard]] imvu::PiRenderTexture* inner() const { return inner_; }
[[nodiscard]] std::uint32_t id() const { return id_; }
private:
imvu::PiRenderTexture* inner_;
std::shared_ptr<Recorder> recorder_;
std::uint32_t id_;
};
class IndexBuffer final : public imvu::PiInternalIndexBuffer {
public:
IndexBuffer(imvu::PiInternalIndexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id);
~IndexBuffer() override;
[[nodiscard]] imvu::PiInternalIndexBuffer* inner() const { return inner_; }
[[nodiscard]] std::uint32_t id() const { return id_; }
private:
imvu::PiInternalIndexBuffer* inner_;
std::shared_ptr<Recorder> recorder_;
std::uint32_t id_;
};
class VertexBuffer final : public imvu::PiInternalVertexBuffer {
public:
VertexBuffer(imvu::PiInternalVertexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id);
~VertexBuffer() override;
void update(const void* data, std::uint32_t stride, std::uint32_t elementSize,
std::uint32_t elementCount) override;
void unknown2() override;
[[nodiscard]] imvu::PiInternalVertexBuffer* inner() const { return inner_; }
[[nodiscard]] std::uint32_t id() const { return id_; }
private:
imvu::PiInternalVertexBuffer* inner_;
std::shared_ptr<Recorder> recorder_;
std::uint32_t id_;
};
// Whether an object is one of ours (live Texture, IndexBuffer or VertexBuffer): the client can also hand the engine
// objects the client renderer made itself (render targets' textures), which must reach it unchanged.
bool ours(const void* object);
// The client renderer's object, and our id, for what the client passes in: ours unwrapped, others unchanged (id 0).
inline imvu::PiRenderTexture* inner(imvu::PiRenderTexture* texture)
{
return ours(texture) ? static_cast<Texture*>(texture)->inner() : texture;
}
inline std::uint32_t id(imvu::PiRenderTexture* texture)
{
return ours(texture) ? static_cast<Texture*>(texture)->id() : 0;
}
inline imvu::PiInternalIndexBuffer* inner(imvu::PiInternalIndexBuffer* buffer)
{
return ours(buffer) ? static_cast<IndexBuffer*>(buffer)->inner() : buffer;
}
inline std::uint32_t id(imvu::PiInternalIndexBuffer* buffer)
{
return ours(buffer) ? static_cast<IndexBuffer*>(buffer)->id() : 0;
}
inline imvu::PiInternalVertexBuffer* inner(imvu::PiInternalVertexBuffer* buffer)
{
return ours(buffer) ? static_cast<VertexBuffer*>(buffer)->inner() : buffer;
}
inline std::uint32_t id(imvu::PiInternalVertexBuffer* buffer)
{
return ours(buffer) ? static_cast<VertexBuffer*>(buffer)->id() : 0;
}
} // namespace t5de
+80
View File
@@ -0,0 +1,80 @@
# 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. For each engine the client
creates (each 3D view, and again on room changes), the adapter:
1. creates a shared memory channel (`Local\t5de-renderer-<pid>-<n>`) holding IMVU's window handle and a frame
count, and an event set after each frame;
2. starts `t5de-renderer.exe --channel <name> --parent <pid>` from its own folder;
3. sets the event at each `endRendering`, and sets `closing` when the engine is destroyed.
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 engine is destroyed or 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 everything to the client's Direct3D renderer (state and resources: it draws render targets and
reads textures back, which the renderer process doesn't yet), but while the renderer process covers the whole view
(`Channel::covering`, set after each frame it presents) the client's renderer skips its clears and draws, decided per
frame; it draws again as soon as the renderer process stops covering the view (the view changes, or the process
fails). Checked in rooms with screenshots of both renderers side by side (the compare view): they match, particles
and the 2D overlay (name tags) included.
For comparing while the client runs (development):
- Views: `T5DE_RENDERER_VIEW` (off, split, full (the default) or compare) when the client starts, or
`%TEMP%\t5de-renderer-view.txt` holding one of those at any time. Compare draws the left half's scene over the
right half, so both renderers show the same thing side by side.
- `%TEMP%\t5de-renderer-shaders\fixed_function.hlsl`, if present, replaces the built-in shaders and is reloaded
when it changes.
- The renderer process runs from a copy (`%TEMP%\t5de-renderer`), so `t5de-renderer.exe` can be replaced while the
client runs; engines started afterwards run the new one (the room's view keeps its engine until the client
restarts).
- The adapter's log (`%TEMP%\t5de-renderer.log`) sums up what the client draws every 300 frames (the first few
times), and logs textures and render targets the client renderer made itself.
- The command stream (`include/t5de/protocol/commands.hpp`) carries state, transforms, lights, fog, textures,
buffers (copied when created or updated) and draws, in chunks through the channel; the adapter waits for each to
be taken, and gives up on a renderer process that stops taking them (the client's renderer carries on).
- The renderer process reproduces IMVU's Direct3D 9 setup (`renderer/shaders/fixed_function.hlsl`), particles
included (point sprites, drawn as instanced squares), with a pipeline per combination of states.
- Speed: on a PC with a graphics card, drawing the whole view (3440x1059, a room of some 220 draws) takes 1.3 to
3.8 ms a frame, and the time from the client finishing a frame to ours being presented is the same (frames don't
queue, none skipped). Earlier, in the split view, the halves couldn't be told apart, even while the camera moved. On the development VM (software rendering, over Remote Desktop) drawing takes 8 to 60 ms, and
the right half visibly trails, mostly from compositing another process's window over Remote Desktop. The
renderer process draws only the newest frame when it falls behind, and keeps at most one frame queued.
Next: render targets (offscreen frames are skipped, and drawn by the client's renderer), and one renderer process for
all of a client's views.
## Building
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.
+27 -3
View File
@@ -59,9 +59,33 @@ 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. `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 `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. 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`, - The data types are in `include/t5de/imvu/types.hpp`, worked out from the Direct3D engine: matrices and
`Pixmap` (`pixmap.dll`), the buffers' contents, `VertexAttributeArray<T>`, `std::vector<PointSprite>` as transforms in Direct3D's row-vector convention, colors as `0xAARRGGBB`, lights (point or directional), blend
Visual C++ 2010 lays it out, and `RenderBlendMode`'s values. 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.
- Still unknown: render targets, `CompoundRegion` (texture update
regions), `newRenderTexture`'s flags, and what `beginRendering` does with a null target. MSVC orders overloaded
virtual methods unlike their declaration, so our declarations never overload virtual methods.
- Memory crosses the boundary only through virtual methods (destructors, control blocks), so each side can use its - 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. own C runtime. Exceptions must not cross it.
+17 -51
View File
@@ -1,54 +1,16 @@
#pragma once #pragma once
// IMVU's renderer interface, PiRenderEngine (SceneWindow.dll), declared to match its vtable: the order of the methods // 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 // 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 // methods; parameter names are ours, from what IMVU's Direct3D engine does with them. See docs/interface.md.
// references are left incomplete until we need their layout. See docs/interface.md.
#include <cstdint> #include <cstdint>
#include "t5de/imvu/resources.hpp"
#include "t5de/imvu/shared_ptr.hpp" #include "t5de/imvu/shared_ptr.hpp"
#include "t5de/imvu/types.hpp"
namespace t5de::imvu { 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 { class PiRenderEngine {
public: public:
PiRenderEngine() = default; PiRenderEngine() = default;
@@ -59,7 +21,7 @@ public:
virtual void resizeFrameBuffer(Vec2i size) = 0; virtual void resizeFrameBuffer(Vec2i size) = 0;
virtual void enable2dFrameBuffer() = 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 setProjection(Vec2i viewport, const Mat4f& projection) = 0;
virtual void setModelSpace(const Transform& model) = 0; // slot 5 virtual void setModelSpace(const Transform& model) = 0; // slot 5
virtual void setViewSpace(const Transform& view) = 0; virtual void setViewSpace(const Transform& view) = 0;
@@ -75,29 +37,33 @@ public:
virtual void setLightingOn(bool on) = 0; virtual void setLightingOn(bool on) = 0;
virtual void setLightProperties(std::uint32_t index, const LightProperties& light) = 0; virtual void setLightProperties(std::uint32_t index, const LightProperties& light) = 0;
virtual void setFogOn(bool on) = 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 setTextureTransformMatrixIdentity(std::uint32_t stage) = 0; // slot 20
virtual void setTextureTransformMatrix(std::uint32_t stage, const TextureTransform& transform) = 0; virtual void setTextureTransformMatrix(std::uint32_t stage, const TextureTransform& transform) = 0;
virtual void setEnvironmentAmbient(std::uint32_t color) = 0; virtual void setEnvironmentAmbient(Color color) = 0; // zero alpha
virtual PiRenderTexture* newRenderTexture(Pixmap* pixmap, std::uint32_t flags) = 0; 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 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, virtual PiInternalVertexBuffer* newStaticVertexBuffer(const void* data, std::uint32_t stride,
std::uint32_t c) = 0; // slot 25 std::uint32_t elementSize,
virtual PiInternalVertexBuffer* newDynamicVertexBuffer(std::uint32_t a, std::uint32_t b) = 0; 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<PiPixmapRenderTarget> newPixmapRenderTarget(Vec2i size) = 0;
virtual SharedPtr<PiTextureRenderTarget> newTextureRenderTarget(Vec2i size) = 0; virtual SharedPtr<PiTextureRenderTarget> newTextureRenderTarget(Vec2i size) = 0;
virtual void beginRendering(PiRenderTarget* target) = 0; virtual void beginRendering(PiRenderTarget* target) = 0;
virtual void endRendering() = 0; // slot 30 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, virtual void drawTriangles(PiInternalIndexBuffer* indices, PiInternalVertexBuffer* positions,
PiInternalVertexBuffer* normals, PiInternalVertexBuffer* texCoords0, PiInternalVertexBuffer* normals, PiInternalVertexBuffer* texCoords,
PiInternalVertexBuffer* texCoords1) = 0; PiInternalVertexBuffer* colors) = 0;
virtual void drawLine(Vec3f from, Vec3f to, std::uint32_t color) = 0; virtual void drawLine(Vec3f from, Vec3f to, std::uint32_t color) = 0;
virtual void drawPointSprites(const PointSpriteVector& sprites) = 0; virtual void drawPointSprites(const PointSpriteVector& sprites) = 0;
virtual void drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount, virtual void drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount,
const std::uint16_t* indices, const VertexAttributeArray<Vec3f>& positions, const std::uint16_t* indices, const VertexAttributeArray<Vec3f>& positions,
const VertexAttributeArray<Vec3f>& normals, const VertexAttributeArray<Vec3f>& normals,
const VertexAttributeArray<Vec2f>& texCoords, const VertexAttributeArray<Vec2f>& texCoords,
const VertexAttributeArray<std::uint32_t>& colors) = 0; // slot 34 const VertexAttributeArray<Color>& colors) = 0; // slot 34
private: private:
// PiRenderEngine's own fields, after the vtable pointer: a magic number IMVU's destructor checks, and a size // PiRenderEngine's own fields, after the vtable pointer: a magic number IMVU's destructor checks, and a size
+46
View File
@@ -0,0 +1,46 @@
#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/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;
};
class PiRenderTarget;
class PiPixmapRenderTarget;
class PiTextureRenderTarget;
} // namespace t5de::imvu
+126
View File
@@ -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
+48
View File
@@ -0,0 +1,48 @@
#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 = 3;
// Which part of the client's 3D view the renderer process draws. Full is the default; the others are for comparing
// with the client's renderer, which then draws the whole view under the renderer process's window.
enum class View : std::uint32_t {
Off, // none
Split, // the right half, to compare with the client's renderer on the left
Full,
Compare, // over the right half, the left half's scene: both renderers side by side, showing the same
};
// A shared memory section the adapter creates for each engine, named by the renderer process's `--channel <name>`
// argument: this header, then `capacity` bytes of data. The adapter sends the command stream (commands.hpp) through
// it in chunks: it waits for the event "<name>-consumed", writes a chunk and sets "<name>-ready"; the renderer
// process copies the chunk out and sets "<name>-consumed" (both auto-reset; consumed starts set).
struct Channel {
std::uint32_t version;
std::uint32_t closing; // nonzero: the engine is gone, the renderer process should exit
std::uint64_t window; // IMVU's 3D window (an HWND; window handles are valid in every process)
std::uint64_t frames; // frames the client has drawn
View view;
std::uint32_t capacity;
std::uint32_t chunkSize;
std::uint32_t chunkFlags; // chunkEndsFrame
// Set by the renderer process while its window covers the whole 3D view and it's presenting frames: the client's
// renderer then needn't draw (the adapter skips its draws; it draws again as soon as this is cleared).
std::uint32_t covering;
std::uint32_t reserved;
};
static_assert(sizeof(Channel) == 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";
} // namespace t5de::protocol
+266
View File
@@ -0,0 +1,266 @@
#pragma once
// The command stream: what the adapter records from IMVU's renderer interface, and the renderer process carries out.
// Each command is a Header, its fixed-size body, then for some a variable-size payload (pixels, vertex data),
// padded to 4 bytes. Built for 32-bit and 64-bit: fixed-size fields only.
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <optional>
#include <span>
#include <vector>
namespace t5de::protocol {
enum class Op : std::uint16_t {
BeginFrame = 1,
EndFrame,
Clear,
SetProjection,
SetModel,
SetView,
SetTextures,
SetState,
SetLight,
SetFog,
SetTextureTransform,
CreateTexture,
UpdateTexture,
DestroyTexture,
CreateBuffer,
UpdateBuffer,
DestroyBuffer,
DrawTriangles,
DrawArrays,
DrawPointSprites,
};
struct Header {
Op op;
std::uint16_t reserved;
std::uint32_t size; // of the body and payload, padded to 4 bytes
};
// Colors are 0xAARRGGBB. Resource ids are the adapter's, per engine; 0 means none.
struct BeginFrame {
std::uint32_t offscreen; // drawn into a render target, not the window: nothing to show
};
struct EndFrame {
std::uint64_t sentAt; // the adapter's QueryPerformanceCounter (the same clock in every process)
};
struct Clear {
std::uint32_t flags; // 1: color, 2: depth (to 1)
std::uint32_t color;
};
// The viewport's size, and a Direct3D projection matrix (row-major, row vectors).
struct SetProjection {
std::int32_t width;
std::int32_t height;
float m[16];
};
// An affine transform (SetModel, SetView): a 3x3 matrix, row-major, then the translation.
struct SetTransform {
float m[12];
};
struct SetTextures {
std::uint32_t texture0;
std::uint32_t texture1;
};
enum class State : std::uint32_t {
DepthTest,
DepthWrite,
Blending,
BlendMode, // 1: alpha, 2: additive, 3: premultiplied, 4: additive alpha
AlphaTest,
AlphaReference, // 0-255: pixels pass if their alpha is greater
Culling,
NormalInverting, // flips which winding faces front
Lighting,
Fog,
Ambient, // a color
};
struct SetState {
State state;
std::uint32_t value;
};
struct SetLight {
std::uint32_t index;
std::uint32_t enabled;
std::uint32_t color;
std::uint32_t directional; // 0: a point light at vector; 1: light coming from direction vector
float vector[3];
};
struct SetFog {
float start;
float end;
std::uint32_t color;
};
// Texture coordinates for a stage: u' = u * m[0] + v * m[1] + m[4], v' = u * m[2] + v * m[3] + m[5].
struct SetTextureTransform {
std::uint32_t stage;
std::uint32_t identity;
float m[6];
};
// Payload: width * height pixels (0xAARRGGBB), rows packed.
struct CreateTexture {
std::uint32_t id;
std::uint32_t width;
std::uint32_t height;
std::uint32_t flags; // 2: wrap u, 4: wrap v (else clamp)
};
struct UpdateTexture {
std::uint32_t id;
std::uint32_t width;
std::uint32_t height;
};
struct DestroyTexture {
std::uint32_t id;
};
enum class BufferKind : std::uint32_t {
Index, // 16-bit indices
Vertex, // positions and normals (3 floats), texture coordinates (2 floats) or colors (0xAARRGGBB)
};
// Payload: count elements of elementSize bytes, packed; empty for a buffer filled later (UpdateBuffer).
struct CreateBuffer {
std::uint32_t id;
BufferKind kind;
std::uint32_t elementSize;
std::uint32_t count;
};
struct UpdateBuffer {
std::uint32_t id;
std::uint32_t elementSize;
std::uint32_t count;
};
struct DestroyBuffer {
std::uint32_t id;
};
// Triangles from buffers: indices and positions are required, the others optional.
struct DrawTriangles {
std::uint32_t indices;
std::uint32_t positions;
std::uint32_t normals;
std::uint32_t texCoords;
std::uint32_t colors;
};
// Triangles from data in the payload, packed: indexCount 16-bit indices (padded to 4 bytes), then vertexCount
// positions, and normals, texture coordinates and colors if present.
struct DrawArrays {
std::uint32_t vertexCount;
std::uint32_t indexCount;
std::uint32_t hasNormals;
std::uint32_t hasTexCoords;
std::uint32_t hasColors;
};
// Particles: payload count sprites of 16 bytes, a position (3 floats) and a color. Each is a camera-facing square,
// viewport height / distance pixels across (1 to 64), textured with the first texture over the whole square.
struct DrawPointSprites {
std::uint32_t count;
};
static_assert(sizeof(Header) == 8 && sizeof(SetProjection) == 72 && sizeof(SetLight) == 28
&& sizeof(DrawArrays) == 20);
inline constexpr std::size_t padded(std::size_t size) { return (size + 3) & ~std::size_t{3}; }
// Appends commands to a byte buffer.
class Writer {
public:
// Appends a command and returns its payload, payloadSize bytes for the caller to fill.
template <class Body>
std::byte* add(Op op, const Body& body, std::size_t payloadSize = 0)
{
static_assert(sizeof(Body) % 4 == 0);
const std::size_t size = sizeof(Body) + padded(payloadSize);
const std::size_t start = bytes_.size();
bytes_.resize(start + sizeof(Header) + size);
const Header header{op, 0, static_cast<std::uint32_t>(size)};
std::memcpy(bytes_.data() + start, &header, sizeof(Header));
std::memcpy(bytes_.data() + start + sizeof(Header), &body, sizeof(Body));
return bytes_.data() + start + sizeof(Header) + sizeof(Body);
}
[[nodiscard]] std::span<const std::byte> bytes() const { return bytes_; }
[[nodiscard]] std::size_t size() const { return bytes_.size(); }
void clear() { bytes_.clear(); }
private:
std::vector<std::byte> bytes_;
};
// One command, as read back.
class Command {
public:
Command(Op op, std::span<const std::byte> data) : op_(op), data_(data) {}
[[nodiscard]] Op op() const { return op_; }
// The body, if the command is long enough to hold one.
template <class Body>
[[nodiscard]] std::optional<Body> body() const
{
if (data_.size() < sizeof(Body)) {
return std::nullopt;
}
Body body;
std::memcpy(&body, data_.data(), sizeof(Body));
return body;
}
template <class Body>
[[nodiscard]] std::span<const std::byte> payload() const
{
return data_.size() < sizeof(Body) ? std::span<const std::byte>{} : data_.subspan(sizeof(Body));
}
private:
Op op_;
std::span<const std::byte> data_;
};
// Reads commands back from a byte buffer; stops at the end, or at a truncated command.
class Reader {
public:
explicit Reader(std::span<const std::byte> bytes) : bytes_(bytes) {}
std::optional<Command> next()
{
if (bytes_.size() < sizeof(Header)) {
return std::nullopt;
}
Header header;
std::memcpy(&header, bytes_.data(), sizeof(Header));
if (bytes_.size() - sizeof(Header) < header.size) {
return std::nullopt;
}
Command command(header.op, bytes_.subspan(sizeof(Header), header.size));
bytes_ = bytes_.subspan(sizeof(Header) + header.size);
return command;
}
private:
std::span<const std::byte> bytes_;
};
} // namespace t5de::protocol
+40
View File
@@ -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 .)
+30
View File
@@ -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
+18
View File
@@ -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
+325
View File
@@ -0,0 +1,325 @@
// t5de-renderer.exe: the renderer process. Started by the adapter (t5deRenderEngine.dll) for each of the client's
// 3D views: receives the client's drawing as commands through a shared memory channel, and draws it with Diligent
// into a child window it places in the client's window (over the right half, for now, to compare with the client's
// own renderer on the left).
#include <windows.h>
#include <d3d11.h>
#include <dxgi.h>
#include <shellapi.h>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <format>
#include <string>
#include <string_view>
#include <vector>
#include "EngineFactoryD3D11.h"
#include "RefCntAutoPtr.hpp"
#include "RenderDeviceD3D11.h"
#include "log.hpp"
#include "scene.hpp"
#include "t5de/protocol/channel.hpp"
namespace {
using namespace Diligent;
using t5de::protocol::Channel;
using t5de::protocol::View;
using t5de::log::info;
// Diligent's messages (shader compilation errors, invalid states, ...), into the log.
void DILIGENT_CALL_TYPE diligent_message(DEBUG_MESSAGE_SEVERITY severity, const Char* message, const Char* function,
const Char*, int)
{
static constexpr const char* severities[] = {"info", "warning", "error", "fatal error"};
info("Diligent {}: {}{}{}", severities[severity < 4 ? severity : 3], function != nullptr ? function : "",
function != nullptr ? ": " : "", message != nullptr ? message : "");
}
struct Arguments {
std::wstring channel;
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"--channel") {
arguments.channel = argv[i + 1];
} else if (name == L"--parent") {
arguments.parent = static_cast<DWORD>(std::wcstoul(argv[i + 1], nullptr, 10));
}
}
LocalFree(argv);
return arguments;
}
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)
{
WNDCLASSEXW windowClass{.cbSize = sizeof(WNDCLASSEXW),
.lpfnWndProc = window_procedure,
.hInstance = GetModuleHandleW(nullptr),
.hCursor = LoadCursorW(nullptr, IDC_ARROW),
.lpszClassName = L"T5DERenderer"};
RegisterClassExW(&windowClass);
return CreateWindowExW(0, windowClass.lpszClassName, L"", WS_CHILD | WS_VISIBLE | WS_DISABLED | WS_CLIPSIBLINGS,
0, 0, 1, 1, parent, nullptr, windowClass.hInstance, nullptr);
}
// The view, as the adapter set it, or as %TEMP%\t5de-renderer-view.txt says while the client runs (off, split, full
// or compare): for comparing the renderers without restarting the client. Read every second or so.
View current_view(View adapter)
{
static View view = adapter;
static ULONGLONG checked = 0;
if (GetTickCount64() - checked < 1000) {
return view;
}
checked = GetTickCount64();
view = adapter;
wchar_t temp[MAX_PATH + 1];
const DWORD length = GetTempPathW(MAX_PATH + 1, temp);
if (length == 0 || length > MAX_PATH) {
return view;
}
const HANDLE file = CreateFileW((std::filesystem::path(temp) / L"t5de-renderer-view.txt").c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) {
return view;
}
char text[16] = {};
DWORD read = 0;
ReadFile(file, text, sizeof(text) - 1, &read, nullptr);
CloseHandle(file);
const std::string_view setting(text, std::strcspn(text, "\r\n "));
view = setting == "off" ? View::Off
: setting == "split" ? View::Split
: setting == "full" ? View::Full
: setting == "compare" ? View::Compare
: adapter;
return view;
}
// Shader changes to try while the client runs: %TEMP%\t5de-renderer-shaders\fixed_function.hlsl, if there is
// one, replaces the built-in shaders, and is read again when it changes (checked every second or so).
void reload_shaders(t5de::Scene& scene)
{
static ULONGLONG checked = 0;
static std::filesystem::file_time_type loaded{};
if (GetTickCount64() - checked < 1000) {
return;
}
checked = GetTickCount64();
std::error_code error;
const auto path = std::filesystem::temp_directory_path(error) / L"t5de-renderer-shaders" / L"fixed_function.hlsl";
const auto modified = std::filesystem::last_write_time(path, error);
if (error || modified == loaded) {
return;
}
const HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) {
return;
}
std::string source(static_cast<std::size_t>(std::filesystem::file_size(path, error)), ' ');
DWORD read = 0;
ReadFile(file, source.data(), static_cast<DWORD>(source.size()), &read, nullptr);
CloseHandle(file);
source.resize(read);
loaded = modified;
scene.setShaderSource(std::move(source));
info("shaders reloaded from {}", path.string());
}
// Where the child window goes in the client's 3D view, and where in the 3D view its scene starts (offset).
struct Placement {
RECT window{};
int offset = 0;
};
Placement placement(HWND parent, View view)
{
Placement place;
GetClientRect(parent, &place.window);
if (view == View::Split || view == View::Compare) {
place.window.left = place.window.right / 2;
}
place.offset = view == View::Compare ? 0 : place.window.left;
if (view == View::Off) {
place.window = {};
}
return place;
}
} // namespace
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
const Arguments arguments = parse_arguments();
if (arguments.channel.empty()) {
return 2;
}
HANDLE mapping = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, arguments.channel.c_str());
auto* channel = mapping == nullptr ? nullptr
: static_cast<Channel*>(MapViewOfFile(mapping, FILE_MAP_ALL_ACCESS, 0, 0, 0));
HANDLE ready = OpenEventW(SYNCHRONIZE, FALSE, (arguments.channel + t5de::protocol::readyEventSuffix).c_str());
HANDLE consumed =
OpenEventW(EVENT_MODIFY_STATE, FALSE, (arguments.channel + t5de::protocol::consumedEventSuffix).c_str());
HANDLE parent = OpenProcess(SYNCHRONIZE, FALSE, arguments.parent);
if (channel == nullptr || ready == nullptr || consumed == nullptr || parent == nullptr
|| channel->version != t5de::protocol::version) {
return 3;
}
const auto* data = reinterpret_cast<const std::byte*>(channel + 1);
const HWND clientWindow = reinterpret_cast<HWND>(static_cast<std::uintptr_t>(channel->window));
const HWND window = create_child_window(clientWindow);
if (window == nullptr) {
return 4;
}
info("started for window {:#x}, view {}", channel->window, static_cast<int>(channel->view));
IEngineFactoryD3D11* factory = GetEngineFactoryD3D11();
factory->SetMessageCallback(diligent_message);
RefCntAutoPtr<IRenderDevice> device;
RefCntAutoPtr<IDeviceContext> context;
EngineD3D11CreateInfo engineInfo;
factory->CreateDeviceAndContextsD3D11(engineInfo, &device, &context);
if (!device) {
info("can't create a Direct3D 11 device");
return 5;
}
// Show each frame as soon as it's drawn: Direct3D 11 otherwise lets 3 frames queue, which the view would trail by.
if (RefCntAutoPtr<IRenderDeviceD3D11> d3d11{device, IID_RenderDeviceD3D11}) {
IDXGIDevice1* dxgi = nullptr;
if (SUCCEEDED(d3d11->GetD3D11Device()->QueryInterface(__uuidof(IDXGIDevice1), reinterpret_cast<void**>(&dxgi)))) {
dxgi->SetMaximumFrameLatency(1);
dxgi->Release();
}
}
RefCntAutoPtr<ISwapChain> swapChain;
SwapChainDesc swapChainDesc;
swapChainDesc.ColorBufferFormat = TEX_FORMAT_RGBA8_UNORM; // no sRGB conversion, like the client's renderer
swapChainDesc.DepthBufferFormat = TEX_FORMAT_D32_FLOAT;
factory->CreateSwapChainD3D11(device, context, swapChainDesc, FullScreenModeDesc{}, Win32NativeWindow{window},
&swapChain);
if (!swapChain) {
info("can't create a swap chain");
return 6;
}
t5de::Scene scene(device, context, swapChain);
// Takes the chunk waiting in the channel (after "ready"), and lets the adapter send the next: appends it to
// frame, and returns whether it ends a frame.
std::vector<std::byte> frame;
const auto take_chunk = [&] {
const std::uint32_t size = channel->chunkSize <= channel->capacity ? channel->chunkSize : 0;
const bool endsFrame = (channel->chunkFlags & t5de::protocol::chunkEndsFrame) != 0;
frame.insert(frame.end(), data, data + size);
SetEvent(consumed);
return endsFrame;
};
LARGE_INTEGER frequency;
QueryPerformanceFrequency(&frequency);
double drawTime = 0;
double latency = 0;
int drawn = 0;
int skipped = 0;
RECT placed{};
for (;;) {
const HANDLE handles[] = {ready, parent};
const DWORD result = MsgWaitForMultipleObjects(2, handles, FALSE, 1000, QS_ALLINPUT);
MSG message;
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
DispatchMessageW(&message);
}
if (result == WAIT_OBJECT_0 + 1 || channel->closing != 0) {
break; // the client exited, or the engine is gone
}
if (result != WAIT_OBJECT_0 || !take_chunk()) {
continue;
}
// If the client has sent more frames meanwhile (drawing fell behind), catch up: carry out the older ones'
// changes without drawing them, and draw only the newest. (An incomplete frame stays for the next round.)
std::size_t complete = frame.size();
while (WaitForSingleObject(ready, 0) == WAIT_OBJECT_0) {
bool ended = take_chunk();
while (!ended && WaitForSingleObject(ready, 1000) == WAIT_OBJECT_0) {
ended = take_chunk();
}
if (!ended) {
break;
}
scene.execute(std::span(frame).first(complete), 0, false);
frame.erase(frame.begin(), frame.begin() + static_cast<std::ptrdiff_t>(complete));
complete = frame.size();
++skipped;
}
// Follow the client's 3D view's size.
const auto [place, offset] = placement(clientWindow, current_view(channel->view));
const int width = place.right - place.left;
const int height = place.bottom - place.top;
if (std::memcmp(&place, &placed, sizeof(RECT)) != 0) {
placed = place;
if (width > 0 && height > 0) {
SetWindowPos(window, nullptr, place.left, place.top, width, height,
SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW);
swapChain->Resize(static_cast<Uint32>(width), static_cast<Uint32>(height));
info("drawing {}x{} at {} in the client's 3D view, its scene from {}", width, height, place.left, offset);
} else {
ShowWindow(window, SW_HIDE);
channel->covering = 0;
info("hidden");
}
}
reload_shaders(scene);
LARGE_INTEGER start, end;
QueryPerformanceCounter(&start);
const bool covers = current_view(channel->view) == View::Full && width > 0 && height > 0;
if (scene.execute(std::span(frame).first(complete), offset, width > 0 && height > 0)) {
swapChain->Present(0);
channel->covering = covers ? 1 : 0; // the client's renderer can stop drawing (or must start again)
QueryPerformanceCounter(&end);
drawTime += static_cast<double>(end.QuadPart - start.QuadPart) / static_cast<double>(frequency.QuadPart);
latency += static_cast<double>(end.QuadPart - static_cast<LONGLONG>(scene.lastSentAt())) /
static_cast<double>(frequency.QuadPart);
if (++drawn == 300) {
info("last 300 frames: {:.1f} ms each to draw and present, {:.1f} ms from the client's end of frame to "
"presented, {} skipped to catch up",
drawTime * 1000 / drawn, latency * 1000 / drawn, skipped);
drawTime = 0;
latency = 0;
drawn = 0;
skipped = 0;
}
}
frame.erase(frame.begin(), frame.begin() + static_cast<std::ptrdiff_t>(complete));
}
channel->covering = 0;
info("exiting");
DestroyWindow(window);
return 0;
}
+687
View File
@@ -0,0 +1,687 @@
#include "scene.hpp"
#include <cstring>
#include <string>
#include "fixed_function_hlsl.hpp"
namespace t5de {
using namespace Diligent;
using protocol::Op;
namespace {
void set_identity(float (&m)[16])
{
std::memset(m, 0, sizeof(m));
m[0] = m[5] = m[10] = m[15] = 1;
}
// An affine transform (3x3 row-major, then translation) as a 4x4 matrix for row vectors.
void set_transform(float (&m)[16], const float (&t)[12])
{
for (int row = 0; row < 3; ++row) {
m[row * 4 + 0] = t[row * 3 + 0];
m[row * 4 + 1] = t[row * 3 + 1];
m[row * 4 + 2] = t[row * 3 + 2];
m[row * 4 + 3] = 0;
}
m[12] = t[9];
m[13] = t[10];
m[14] = t[11];
m[15] = 1;
}
// u' = u * m[0] + v * m[1] + m[4], v' = u * m[2] + v * m[3] + m[5], as two rows dotted with (u, v, 1).
void set_texture_transform(float (&rows)[8], const float (&m)[6])
{
const float values[8] = {m[0], m[1], m[4], 0, m[2], m[3], m[5], 0};
std::memcpy(rows, values, sizeof(rows));
}
void set_color(float (&rgba)[4], std::uint32_t color)
{
rgba[0] = static_cast<float>((color >> 16) & 0xff) / 255.0f;
rgba[1] = static_cast<float>((color >> 8) & 0xff) / 255.0f;
rgba[2] = static_cast<float>(color & 0xff) / 255.0f;
rgba[3] = static_cast<float>(color >> 24) / 255.0f;
}
constexpr std::uint32_t hasNormals = 1;
constexpr std::uint32_t hasTexCoords = 2;
constexpr std::uint32_t hasColors = 4;
} // namespace
Scene::Scene(IRenderDevice* device, IDeviceContext* context, ISwapChain* swapChain)
: device_(device), context_(context), swapChain_(swapChain), shaderSource_(fixedFunctionHlsl)
{
BufferDesc constants;
constants.Name = "Constants";
constants.Size = sizeof(Constants);
constants.Usage = USAGE_DYNAMIC;
constants.BindFlags = BIND_UNIFORM_BUFFER;
constants.CPUAccessFlags = CPU_ACCESS_WRITE;
device_->CreateBuffer(constants, nullptr, &constantBuffer_);
// IMVU's textures: bilinear, no mipmaps, wrapped or clamped per texture.
for (int wrap = 0; wrap < 4; ++wrap) {
SamplerDesc sampler;
sampler.MinFilter = FILTER_TYPE_LINEAR;
sampler.MagFilter = FILTER_TYPE_LINEAR;
sampler.MipFilter = FILTER_TYPE_POINT;
sampler.AddressU = (wrap & 1) != 0 ? TEXTURE_ADDRESS_WRAP : TEXTURE_ADDRESS_CLAMP;
sampler.AddressV = (wrap & 2) != 0 ? TEXTURE_ADDRESS_WRAP : TEXTURE_ADDRESS_CLAMP;
device_->CreateSampler(sampler, &samplers_[wrap]);
}
set_identity(constants_.world);
set_identity(constants_.view);
set_identity(constants_.projection);
const float identity[6] = {1, 0, 0, 1, 0, 0};
set_texture_transform(constants_.texTransform0, identity);
set_texture_transform(constants_.texTransform1, identity);
constants_.flags[2] = 127; // the client's alpha reference
}
void Scene::setShaderSource(std::string source)
{
shaderSource_ = std::move(source);
shaders_.clear();
pipelines_.clear();
}
bool Scene::execute(std::span<const std::byte> commands, int offsetX, bool draw)
{
offsetX_ = offsetX;
draw_ = draw;
bool drew = false;
protocol::Reader reader(commands);
while (const auto command = reader.next()) {
switch (command->op()) {
case Op::BeginFrame:
if (const auto begin = command->body<protocol::BeginFrame>()) {
onscreen_ = begin->offscreen == 0 && draw_;
if (onscreen_) {
ITextureView* target = swapChain_->GetCurrentBackBufferRTV();
context_->SetRenderTargets(1, &target, swapChain_->GetDepthBufferDSV(),
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
}
}
break;
case Op::EndFrame:
if (const auto end = command->body<protocol::EndFrame>()) {
lastSentAt_ = end->sentAt;
}
drew = drew || onscreen_;
onscreen_ = false;
break;
case Op::Clear:
if (const auto clear = command->body<protocol::Clear>(); clear && onscreen_) {
if ((clear->flags & 1) != 0) {
float color[4];
set_color(color, clear->color);
context_->ClearRenderTarget(swapChain_->GetCurrentBackBufferRTV(), color,
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
}
if ((clear->flags & 2) != 0) {
context_->ClearDepthStencil(swapChain_->GetDepthBufferDSV(), CLEAR_DEPTH_FLAG, 1.0f, 0,
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
}
}
break;
case Op::SetProjection:
if (const auto projection = command->body<protocol::SetProjection>()) {
setProjection(*projection);
}
break;
case Op::SetModel:
if (const auto model = command->body<protocol::SetTransform>()) {
set_transform(constants_.world, model->m);
}
break;
case Op::SetView:
if (const auto view = command->body<protocol::SetTransform>()) {
set_transform(constants_.view, view->m);
}
break;
case Op::SetTextures:
if (const auto textures = command->body<protocol::SetTextures>()) {
state_.texture0 = textures->texture0;
state_.texture1 = textures->texture1;
}
break;
case Op::SetState:
if (const auto state = command->body<protocol::SetState>()) {
setState(*state);
}
break;
case Op::SetLight:
if (const auto light = command->body<protocol::SetLight>(); light && light->index < 8) {
set_color(constants_.lightColor[light->index], light->color);
constants_.lightColor[light->index][3] = light->enabled != 0 ? 1.0f : 0.0f;
std::memcpy(constants_.lightVector[light->index], light->vector, sizeof(light->vector));
constants_.lightVector[light->index][3] = light->directional != 0 ? 1.0f : 0.0f;
}
break;
case Op::SetFog:
if (const auto fog = command->body<protocol::SetFog>()) {
constants_.fog[0] = fog->start;
constants_.fog[1] = fog->end;
set_color(constants_.fogColor, fog->color);
}
break;
case Op::SetTextureTransform:
if (const auto transform = command->body<protocol::SetTextureTransform>(); transform && transform->stage < 2) {
set_texture_transform(transform->stage == 0 ? constants_.texTransform0 : constants_.texTransform1,
transform->m);
}
break;
case Op::CreateTexture:
createTexture(*command);
break;
case Op::UpdateTexture:
updateTexture(*command);
break;
case Op::DestroyTexture:
if (const auto destroy = command->body<protocol::DestroyTexture>()) {
textures_.erase(destroy->id);
}
break;
case Op::CreateBuffer:
createBuffer(*command);
break;
case Op::UpdateBuffer:
updateBuffer(*command);
break;
case Op::DestroyBuffer:
if (const auto destroy = command->body<protocol::DestroyBuffer>()) {
buffers_.erase(destroy->id);
}
break;
case Op::DrawTriangles:
if (const auto triangles = command->body<protocol::DrawTriangles>(); triangles && onscreen_) {
drawTriangles(*triangles);
}
break;
case Op::DrawArrays:
if (onscreen_) {
drawArrays(*command);
}
break;
case Op::DrawPointSprites:
if (onscreen_) {
drawPointSprites(*command);
}
break;
}
}
return drew;
}
void Scene::setProjection(const protocol::SetProjection& projection)
{
std::memcpy(constants_.projection, projection.m, sizeof(constants_.projection));
// The client's projections give depth from -1 to 1, as OpenGL does; IMVU's Direct3D renderer maps it to 0 to 1
// (z' = z / 2 + w / 2) before setting them, and so do we. (It also shifts them half a pixel, which Direct3D 9
// needs and Direct3D 11 doesn't.)
for (int row = 0; row < 4; ++row) {
float* m = constants_.projection + row * 4;
m[2] = 0.5f * m[2] + 0.5f * m[3];
}
constants_.viewport[0] = static_cast<float>(projection.width);
constants_.viewport[1] = static_cast<float>(projection.height);
if (!onscreen_) {
return;
}
// The client's projection covers its whole 3D view; the window shows part of it, from offsetX.
const SwapChainDesc& window = swapChain_->GetDesc();
Viewport viewport;
viewport.TopLeftX = static_cast<float>(-offsetX_);
viewport.TopLeftY = 0;
viewport.Width = static_cast<float>(projection.width);
viewport.Height = static_cast<float>(projection.height);
viewport.MinDepth = 0;
viewport.MaxDepth = 1;
context_->SetViewports(1, &viewport, window.Width, window.Height);
}
void Scene::setState(const protocol::SetState& state)
{
const bool on = state.value != 0;
switch (state.state) {
case protocol::State::DepthTest:
state_.depthTest = on;
break;
case protocol::State::DepthWrite:
state_.depthWrite = on;
break;
case protocol::State::Blending:
state_.blending = on;
break;
case protocol::State::BlendMode:
if (state.value >= 1 && state.value <= 4) {
state_.blendMode = state.value;
}
break;
case protocol::State::AlphaTest:
constants_.flags[1] = on ? 1.0f : 0.0f;
break;
case protocol::State::AlphaReference:
constants_.flags[2] = static_cast<float>(state.value);
break;
case protocol::State::Culling:
state_.culling = on;
break;
case protocol::State::NormalInverting:
state_.normalInverting = on;
break;
case protocol::State::Lighting:
constants_.flags[0] = on ? 1.0f : 0.0f;
break;
case protocol::State::Fog:
constants_.fog[2] = on ? 1.0f : 0.0f;
break;
case protocol::State::Ambient:
set_color(constants_.ambient, state.value);
break;
}
}
void Scene::createTexture(const protocol::Command& command)
{
const auto create = command.body<protocol::CreateTexture>();
const auto pixels = command.payload<protocol::CreateTexture>();
if (!create || create->width == 0 || create->height == 0
|| pixels.size() < std::size_t{create->width} * create->height * 4) {
return;
}
TextureDesc desc;
desc.Name = "Client texture";
desc.Type = RESOURCE_DIM_TEX_2D;
desc.Width = create->width;
desc.Height = create->height;
desc.MipLevels = 1;
desc.Format = TEX_FORMAT_BGRA8_UNORM; // 0xAARRGGBB in memory
desc.BindFlags = BIND_SHADER_RESOURCE;
desc.Usage = USAGE_DEFAULT;
TextureSubResData level;
level.pData = pixels.data();
level.Stride = std::uint64_t{create->width} * 4;
TextureData data;
data.pSubResources = &level;
data.NumSubresources = 1;
Texture texture{.width = create->width, .height = create->height, .flags = create->flags};
device_->CreateTexture(desc, &data, &texture.texture);
if (!texture.texture) {
return;
}
texture.texture->GetDefaultView(TEXTURE_VIEW_SHADER_RESOURCE)
->SetSampler(samplers_[((create->flags & 2) != 0 ? 1 : 0) | ((create->flags & 4) != 0 ? 2 : 0)]);
textures_[create->id] = std::move(texture);
}
void Scene::updateTexture(const protocol::Command& command)
{
const auto update = command.body<protocol::UpdateTexture>();
const auto pixels = command.payload<protocol::UpdateTexture>();
if (!update || pixels.size() < std::size_t{update->width} * update->height * 4) {
return;
}
const auto found = textures_.find(update->id);
if (found == textures_.end()) {
return;
}
Texture& texture = found->second;
if (texture.width != update->width || texture.height != update->height) {
// A different size: make it again, keeping its sampling.
protocol::Writer writer;
std::memcpy(writer.add(Op::CreateTexture,
protocol::CreateTexture{update->id, update->width, update->height, texture.flags},
pixels.size()),
pixels.data(), pixels.size());
protocol::Reader reader(writer.bytes());
createTexture(*reader.next());
return;
}
Box box;
box.MaxX = update->width;
box.MaxY = update->height;
TextureSubResData level;
level.pData = pixels.data();
level.Stride = std::uint64_t{update->width} * 4;
context_->UpdateTexture(texture.texture, 0, 0, box, level, RESOURCE_STATE_TRANSITION_MODE_TRANSITION,
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
}
void Scene::createBuffer(const protocol::Command& command)
{
const auto create = command.body<protocol::CreateBuffer>();
if (!create || create->count == 0 || create->elementSize == 0) {
return;
}
const std::size_t size = std::size_t{create->elementSize} * create->count;
const auto contents = command.payload<protocol::CreateBuffer>();
const bool filled = contents.size() >= size;
BufferDesc desc;
desc.Name = create->kind == protocol::BufferKind::Index ? "Client indices" : "Client vertices";
desc.Size = size;
desc.BindFlags = create->kind == protocol::BufferKind::Index ? BIND_INDEX_BUFFER : BIND_VERTEX_BUFFER;
desc.Usage = filled ? USAGE_IMMUTABLE : USAGE_DEFAULT; // dynamic buffers are filled by updates
BufferData data;
data.pData = contents.data();
data.DataSize = size;
Buffer buffer{.kind = create->kind, .elementSize = create->elementSize, .count = create->count};
device_->CreateBuffer(desc, filled ? &data : nullptr, &buffer.buffer);
if (buffer.buffer) {
buffers_[create->id] = std::move(buffer);
}
}
void Scene::updateBuffer(const protocol::Command& command)
{
const auto update = command.body<protocol::UpdateBuffer>();
const auto contents = command.payload<protocol::UpdateBuffer>();
if (!update) {
return;
}
const auto found = buffers_.find(update->id);
const std::size_t size = std::size_t{update->elementSize} * update->count;
if (found == buffers_.end() || contents.size() < size || found->second.elementSize != update->elementSize
|| found->second.count != update->count || found->second.buffer->GetDesc().Usage != USAGE_DEFAULT) {
return;
}
context_->UpdateBuffer(found->second.buffer, 0, size, contents.data(), RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
}
ITextureView* Scene::textureView(std::uint32_t id)
{
const auto found = textures_.find(id);
return found == textures_.end() ? nullptr : found->second.texture->GetDefaultView(TEXTURE_VIEW_SHADER_RESOURCE);
}
void Scene::drawTriangles(const protocol::DrawTriangles& draw)
{
const auto buffer = [this](std::uint32_t id, protocol::BufferKind kind, std::uint32_t elementSize) -> Buffer* {
const auto found = buffers_.find(id);
if (id == 0 || found == buffers_.end() || found->second.kind != kind || found->second.elementSize != elementSize) {
return nullptr;
}
return &found->second;
};
Buffer* indices = buffer(draw.indices, protocol::BufferKind::Index, 2);
Buffer* positions = buffer(draw.positions, protocol::BufferKind::Vertex, 12);
if (indices == nullptr || positions == nullptr) {
return;
}
// Optional streams count only if they cover every vertex.
const auto optional = [&](std::uint32_t id, std::uint32_t elementSize) -> IBuffer* {
Buffer* found = buffer(id, protocol::BufferKind::Vertex, elementSize);
return found != nullptr && found->count >= positions->count ? found->buffer.RawPtr() : nullptr;
};
this->draw(indices->buffer, indices->count, positions->buffer, optional(draw.normals, 12),
optional(draw.texCoords, 8), optional(draw.colors, 4));
}
void Scene::drawArrays(const protocol::Command& command)
{
const auto arrays = command.body<protocol::DrawArrays>();
const auto payload = command.payload<protocol::DrawArrays>();
if (!arrays || arrays->vertexCount == 0 || arrays->indexCount == 0) {
return;
}
const std::size_t indexBytes = protocol::padded(std::size_t{arrays->indexCount} * 2);
const std::size_t vertices = arrays->vertexCount;
if (payload.size() < indexBytes + vertices * (12 + (arrays->hasNormals ? 12 : 0) + (arrays->hasTexCoords ? 8 : 0) +
(arrays->hasColors ? 4 : 0))) {
return;
}
const auto make = [this](const std::byte* data, std::size_t size, BIND_FLAGS bind) {
BufferDesc desc;
desc.Name = "Client arrays";
desc.Size = size;
desc.BindFlags = bind;
desc.Usage = USAGE_IMMUTABLE;
BufferData contents;
contents.pData = data;
contents.DataSize = size;
RefCntAutoPtr<IBuffer> buffer;
device_->CreateBuffer(desc, &contents, &buffer);
return buffer;
};
const std::byte* next = payload.data();
const auto indices = make(next, std::size_t{arrays->indexCount} * 2, BIND_INDEX_BUFFER);
next += indexBytes;
const auto positions = make(next, vertices * 12, BIND_VERTEX_BUFFER);
next += vertices * 12;
RefCntAutoPtr<IBuffer> normals, texCoords, colors;
if (arrays->hasNormals) {
normals = make(next, vertices * 12, BIND_VERTEX_BUFFER);
next += vertices * 12;
}
if (arrays->hasTexCoords) {
texCoords = make(next, vertices * 8, BIND_VERTEX_BUFFER);
next += vertices * 8;
}
if (arrays->hasColors) {
colors = make(next, vertices * 4, BIND_VERTEX_BUFFER);
}
draw(indices, arrays->indexCount, positions, normals, texCoords, colors);
}
void Scene::draw(IBuffer* indices, std::uint32_t indexCount, IBuffer* positions, IBuffer* normals, IBuffer* texCoords,
IBuffer* colors)
{
if (indices == nullptr || positions == nullptr || indexCount == 0) {
return;
}
const std::uint32_t streams =
(normals != nullptr ? hasNormals : 0) | (texCoords != nullptr ? hasTexCoords : 0) | (colors != nullptr ? hasColors : 0);
if (prepare(streams, false) == nullptr) {
return;
}
IBuffer* vertexBuffers[4];
std::uint32_t count = 0;
for (IBuffer* stream : {positions, normals, texCoords, colors}) {
if (stream != nullptr) {
vertexBuffers[count++] = stream;
}
}
context_->SetVertexBuffers(0, count, vertexBuffers, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION,
SET_VERTEX_BUFFERS_FLAG_RESET);
context_->SetIndexBuffer(indices, 0, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
DrawIndexedAttribs attributes;
attributes.NumIndices = indexCount;
attributes.IndexType = VT_UINT16;
context_->DrawIndexed(attributes);
}
Scene::Pipeline* Scene::prepare(std::uint32_t streams, bool sprites)
{
// Like the client: no textures, the first, or both (the second only with a first).
ITextureView* texture0 = textureView(state_.texture0);
ITextureView* texture1 = texture0 != nullptr ? textureView(state_.texture1) : nullptr;
const std::uint32_t textures = texture0 == nullptr ? 0 : texture1 == nullptr ? 1 : 2;
Pipeline* selected = pipeline(streams, textures, sprites);
if (selected == nullptr) {
return nullptr;
}
void* mapped = nullptr;
context_->MapBuffer(constantBuffer_, MAP_WRITE, MAP_FLAG_DISCARD, mapped);
if (mapped == nullptr) {
return nullptr;
}
std::memcpy(mapped, &constants_, sizeof(constants_));
context_->UnmapBuffer(constantBuffer_, MAP_WRITE);
if (selected->texture0 != nullptr) {
selected->texture0->Set(texture0);
}
if (selected->texture1 != nullptr) {
selected->texture1->Set(texture1);
}
context_->SetPipelineState(selected->state);
context_->CommitShaderResources(selected->bindings, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
return selected;
}
void Scene::drawPointSprites(const protocol::Command& command)
{
const auto sprites = command.body<protocol::DrawPointSprites>();
const auto payload = command.payload<protocol::DrawPointSprites>();
if (!sprites || sprites->count == 0 || payload.size() < std::size_t{sprites->count} * 16) {
return;
}
BufferDesc desc;
desc.Name = "Client particles";
desc.Size = std::size_t{sprites->count} * 16;
desc.BindFlags = BIND_VERTEX_BUFFER;
desc.Usage = USAGE_IMMUTABLE;
BufferData contents;
contents.pData = payload.data();
contents.DataSize = desc.Size;
RefCntAutoPtr<IBuffer> buffer;
device_->CreateBuffer(desc, &contents, &buffer);
if (!buffer || prepare(0, true) == nullptr) {
return;
}
IBuffer* buffers[] = {buffer};
context_->SetVertexBuffers(0, 1, buffers, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION,
SET_VERTEX_BUFFERS_FLAG_RESET);
DrawAttribs attributes;
attributes.NumVertices = 4;
attributes.NumInstances = sprites->count;
context_->Draw(attributes);
}
const Scene::Shaders& Scene::shaders(std::uint32_t streams, std::uint32_t textures, bool sprites)
{
const std::uint32_t key = streams | (textures << 3) | (sprites ? 1u << 5 : 0);
if (const auto found = shaders_.find(key); found != shaders_.end()) {
return found->second;
}
const std::string source = std::string("#define HAS_NORMALS ") + ((streams & hasNormals) != 0 ? "1" : "0") +
"\n#define HAS_TEX_COORDS " + ((streams & hasTexCoords) != 0 ? "1" : "0") +
"\n#define HAS_COLORS " + ((streams & hasColors) != 0 ? "1" : "0") +
"\n#define TEXTURES " + std::to_string(textures) + "\n" + shaderSource_;
Shaders created;
ShaderCreateInfo info;
info.SourceLanguage = SHADER_SOURCE_LANGUAGE_HLSL;
info.Desc.UseCombinedTextureSamplers = true; // g_TextureN with g_TextureN_sampler, set from each texture's view
info.Source = source.c_str();
info.Desc.ShaderType = SHADER_TYPE_VERTEX;
info.Desc.Name = sprites ? "Fixed function particles (vertex)" : "Fixed function (vertex)";
info.EntryPoint = sprites ? "vs_sprite" : "vs_main";
device_->CreateShader(info, &created.vertex);
info.Desc.ShaderType = SHADER_TYPE_PIXEL;
info.Desc.Name = "Fixed function (pixel)";
info.EntryPoint = "ps_main";
device_->CreateShader(info, &created.pixel);
return shaders_[key] = std::move(created);
}
Scene::Pipeline* Scene::pipeline(std::uint32_t streams, std::uint32_t textures, bool sprites)
{
const std::uint32_t blend = state_.blending ? state_.blendMode : 0;
const bool culling = state_.culling && !sprites; // point sprites aren't culled
const std::uint32_t key = streams | (textures << 3) | (blend << 5) | (state_.depthTest ? 1u << 8 : 0) |
(state_.depthWrite ? 1u << 9 : 0) | (culling ? 1u << 10 : 0) |
(state_.normalInverting ? 1u << 11 : 0) | (sprites ? 1u << 12 : 0);
if (const auto found = pipelines_.find(key); found != pipelines_.end()) {
return found->second.state ? &found->second : nullptr;
}
Pipeline& entry = pipelines_[key];
const Shaders& compiled = shaders(streams, textures, sprites);
if (!compiled.vertex || !compiled.pixel) {
return nullptr;
}
GraphicsPipelineStateCreateInfo info;
info.PSODesc.Name = "Fixed function";
info.PSODesc.PipelineType = PIPELINE_TYPE_GRAPHICS;
const SwapChainDesc& window = swapChain_->GetDesc();
GraphicsPipelineDesc& graphics = info.GraphicsPipeline;
graphics.NumRenderTargets = 1;
graphics.RTVFormats[0] = window.ColorBufferFormat;
graphics.DSVFormat = window.DepthBufferFormat;
graphics.PrimitiveTopology = sprites ? PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP : PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
// The client culls clockwise triangles, or counter-clockwise ones when normals are inverted (mirrored objects).
graphics.RasterizerDesc.CullMode = culling ? CULL_MODE_BACK : CULL_MODE_NONE;
graphics.RasterizerDesc.FrontCounterClockwise = !state_.normalInverting;
graphics.DepthStencilDesc.DepthEnable = state_.depthTest;
graphics.DepthStencilDesc.DepthWriteEnable = state_.depthWrite;
graphics.DepthStencilDesc.DepthFunc = COMPARISON_FUNC_LESS;
RenderTargetBlendDesc& blending = graphics.BlendDesc.RenderTargets[0];
blending.BlendEnable = blend != 0;
if (blend != 0) {
const BLEND_FACTOR source = blend == 1 || blend == 4 ? BLEND_FACTOR_SRC_ALPHA : BLEND_FACTOR_ONE;
const BLEND_FACTOR destination = blend == 2 || blend == 4 ? BLEND_FACTOR_ONE : BLEND_FACTOR_INV_SRC_ALPHA;
blending.SrcBlend = blending.SrcBlendAlpha = source;
blending.DestBlend = blending.DestBlendAlpha = destination;
blending.BlendOp = blending.BlendOpAlpha = BLEND_OPERATION_ADD;
}
LayoutElement elements[4];
std::uint32_t count = 0;
if (sprites) {
// One buffer of sprites, each a position and a color, read once per square (instance).
elements[0] = LayoutElement{0, 0, 3, VT_FLOAT32, False};
elements[1] = LayoutElement{3, 0, 4, VT_UINT8, True};
elements[1].RelativeOffset = 12;
for (LayoutElement& element : std::span(elements).first(2)) {
element.Stride = 16;
element.Frequency = INPUT_ELEMENT_FREQUENCY_PER_INSTANCE;
element.InstanceDataStepRate = 1;
}
count = 2;
} else {
elements[count] = LayoutElement{0, count, 3, VT_FLOAT32, False};
++count;
}
if (!sprites && (streams & hasNormals) != 0) {
elements[count] = LayoutElement{1, count, 3, VT_FLOAT32, False};
++count;
}
if (!sprites && (streams & hasTexCoords) != 0) {
elements[count] = LayoutElement{2, count, 2, VT_FLOAT32, False};
++count;
}
if (!sprites && (streams & hasColors) != 0) {
elements[count] = LayoutElement{3, count, 4, VT_UINT8, True};
++count;
}
graphics.InputLayout.LayoutElements = elements;
graphics.InputLayout.NumElements = count;
info.pVS = compiled.vertex;
info.pPS = compiled.pixel;
const ShaderResourceVariableDesc variables[] = {
{SHADER_TYPE_PIXEL, "g_Texture0", SHADER_RESOURCE_VARIABLE_TYPE_DYNAMIC},
{SHADER_TYPE_PIXEL, "g_Texture1", SHADER_RESOURCE_VARIABLE_TYPE_DYNAMIC},
};
info.PSODesc.ResourceLayout.DefaultVariableType = SHADER_RESOURCE_VARIABLE_TYPE_STATIC;
info.PSODesc.ResourceLayout.Variables = variables;
info.PSODesc.ResourceLayout.NumVariables = textures;
device_->CreateGraphicsPipelineState(info, &entry.state);
if (!entry.state) {
return nullptr;
}
for (const SHADER_TYPE stage : {SHADER_TYPE_VERTEX, SHADER_TYPE_PIXEL}) {
if (IShaderResourceVariable* constants = entry.state->GetStaticVariableByName(stage, "Constants")) {
constants->Set(constantBuffer_);
}
}
entry.state->CreateShaderResourceBinding(&entry.bindings, true);
if (textures >= 1) {
entry.texture0 = entry.bindings->GetVariableByName(SHADER_TYPE_PIXEL, "g_Texture0");
}
if (textures >= 2) {
entry.texture1 = entry.bindings->GetVariableByName(SHADER_TYPE_PIXEL, "g_Texture1");
}
return &entry;
}
} // namespace t5de
+128
View File
@@ -0,0 +1,128 @@
#pragma once
// Carries out the command stream (include/t5de/protocol/commands.hpp) with Diligent, reproducing IMVU's Direct3D 9
// look (shaders/fixed_function.hlsl): the client's textures and buffers by id, its state as it sets it, a pipeline
// for each combination of states it draws with.
#include <cstddef>
#include <cstdint>
#include <span>
#include <string>
#include <unordered_map>
#include "EngineFactoryD3D11.h"
#include "RefCntAutoPtr.hpp"
#include "t5de/protocol/commands.hpp"
namespace t5de {
class Scene {
public:
Scene(Diligent::IRenderDevice* device, Diligent::IDeviceContext* context, Diligent::ISwapChain* swapChain);
// Carries out one frame's commands. offsetX is where the window starts within the client's 3D view; without draw,
// only the frame's changes are made (textures, buffers, state). True if the frame was drawn into the window (to
// present).
bool execute(std::span<const std::byte> commands, int offsetX, bool draw);
// Replaces the shaders' source (the built-in one by default): for trying changes while the client runs.
void setShaderSource(std::string source);
// When the adapter sent the last frame carried out (QueryPerformanceCounter).
[[nodiscard]] std::uint64_t lastSentAt() const { return lastSentAt_; }
private:
struct Texture {
Diligent::RefCntAutoPtr<Diligent::ITexture> texture;
std::uint32_t width = 0;
std::uint32_t height = 0;
std::uint32_t flags = 0;
};
struct Buffer {
Diligent::RefCntAutoPtr<Diligent::IBuffer> buffer;
protocol::BufferKind kind = protocol::BufferKind::Vertex;
std::uint32_t elementSize = 0;
std::uint32_t count = 0;
};
struct Pipeline {
Diligent::RefCntAutoPtr<Diligent::IPipelineState> state;
Diligent::RefCntAutoPtr<Diligent::IShaderResourceBinding> bindings;
Diligent::IShaderResourceVariable* texture0 = nullptr;
Diligent::IShaderResourceVariable* texture1 = nullptr;
};
struct Shaders {
Diligent::RefCntAutoPtr<Diligent::IShader> vertex;
Diligent::RefCntAutoPtr<Diligent::IShader> pixel;
};
// Mirrors the shader's constant buffer.
struct Constants {
float world[16];
float view[16];
float projection[16];
float texTransform0[8];
float texTransform1[8];
float ambient[4];
float lightColor[8][4];
float lightVector[8][4];
float fogColor[4];
float fog[4];
float flags[4];
float viewport[4];
};
// What a draw needs from the state, as set by the client.
struct State {
bool depthTest = true;
bool depthWrite = true;
bool blending = false;
std::uint32_t blendMode = 1;
bool culling = true;
bool normalInverting = false;
std::uint32_t texture0 = 0;
std::uint32_t texture1 = 0;
};
void createTexture(const protocol::Command& command);
void updateTexture(const protocol::Command& command);
void createBuffer(const protocol::Command& command);
void updateBuffer(const protocol::Command& command);
void setProjection(const protocol::SetProjection& projection);
void setState(const protocol::SetState& state);
void drawTriangles(const protocol::DrawTriangles& draw);
void drawArrays(const protocol::Command& command);
void drawPointSprites(const protocol::Command& command);
// Draws indexCount indices from index, with vertex streams (positions required, the others may be null).
void draw(Diligent::IBuffer* indices, std::uint32_t indexCount, Diligent::IBuffer* positions,
Diligent::IBuffer* normals, Diligent::IBuffer* texCoords, Diligent::IBuffer* colors);
// The pipeline for the current state, with these vertex streams and textures, for triangles or particles.
Pipeline* pipeline(std::uint32_t streams, std::uint32_t textures, bool sprites);
const Shaders& shaders(std::uint32_t streams, std::uint32_t textures, bool sprites);
// Picks the pipeline for the current state and sets it up for a draw: constants, textures. Null if it can't.
Pipeline* prepare(std::uint32_t streams, bool sprites);
Diligent::ITextureView* textureView(std::uint32_t id);
Diligent::RefCntAutoPtr<Diligent::IRenderDevice> device_;
Diligent::RefCntAutoPtr<Diligent::IDeviceContext> context_;
Diligent::RefCntAutoPtr<Diligent::ISwapChain> swapChain_;
Diligent::RefCntAutoPtr<Diligent::IBuffer> constantBuffer_;
Diligent::RefCntAutoPtr<Diligent::ISampler> samplers_[4]; // by wrap u (1) and wrap v (2)
std::unordered_map<std::uint32_t, Texture> textures_;
std::unordered_map<std::uint32_t, Buffer> buffers_;
std::unordered_map<std::uint32_t, Shaders> shaders_;
std::unordered_map<std::uint32_t, Pipeline> pipelines_;
std::string shaderSource_;
Constants constants_{};
State state_;
bool onscreen_ = false; // in a frame drawn into the window
bool draw_ = true;
std::uint64_t lastSentAt_ = 0;
int offsetX_ = 0;
};
} // namespace t5de
+4
View File
@@ -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";
+146
View File
@@ -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;
}
-290
View File
@@ -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
-70
View File
@@ -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
+4
View File
@@ -1,3 +1,7 @@
add_executable(shared_ptr_test shared_ptr_test.cpp) add_executable(shared_ptr_test shared_ptr_test.cpp)
target_link_libraries(shared_ptr_test PRIVATE t5de::imvu t5de::options) target_link_libraries(shared_ptr_test PRIVATE t5de::imvu t5de::options)
add_test(NAME shared_ptr COMMAND shared_ptr_test) 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)
+61
View File
@@ -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;
}