2 Commits
Author SHA1 Message Date
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
13 changed files with 292 additions and 46 deletions

No files matched your search

+46 -2
View File
@@ -5,6 +5,7 @@
#include <bit> #include <bit>
#include <cmath> #include <cmath>
#include <cstring> #include <cstring>
#include <vector>
#include "log.hpp" #include "log.hpp"
#include "resources.hpp" #include "resources.hpp"
@@ -53,6 +54,9 @@ void Engine::clear(std::uint32_t flags, imvu::Color color)
void Engine::setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection) void Engine::setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection)
{ {
inner_->setProjection(viewport, projection); inner_->setProjection(viewport, projection);
if (projection.m[2][3] == 0.0f) {
++stats_.flat;
}
protocol::SetProjection command{viewport.x, viewport.y, {}}; protocol::SetProjection command{viewport.x, viewport.y, {}};
std::memcpy(command.m, &projection.m[0][0], sizeof(command.m)); std::memcpy(command.m, &projection.m[0][0], sizeof(command.m));
recorder_->add(Op::SetProjection, command); recorder_->add(Op::SetProjection, command);
@@ -73,6 +77,12 @@ void Engine::setViewSpace(const imvu::Transform& view)
void Engine::setTextures(imvu::PiRenderTexture* texture0, imvu::PiRenderTexture* texture1) void Engine::setTextures(imvu::PiRenderTexture* texture0, imvu::PiRenderTexture* texture1)
{ {
inner_->setTextures(inner(texture0), inner(texture1)); inner_->setTextures(inner(texture0), inner(texture1));
for (imvu::PiRenderTexture* texture : {texture0, texture1}) {
if (texture != nullptr && id(texture) == 0 && foreignLogged_ < 20) {
++foreignLogged_;
log::info("engine: frame {}: a texture the client renderer made ({})", frames_, static_cast<void*>(texture));
}
}
recorder_->add(Op::SetTextures, protocol::SetTextures{id(texture0), id(texture1)}); recorder_->add(Op::SetTextures, protocol::SetTextures{id(texture0), id(texture1)});
} }
@@ -219,17 +229,23 @@ imvu::PiInternalVertexBuffer* Engine::newDynamicVertexBuffer(std::uint32_t eleme
imvu::SharedPtr<imvu::PiPixmapRenderTarget> Engine::newPixmapRenderTarget(imvu::Vec2i size) imvu::SharedPtr<imvu::PiPixmapRenderTarget> Engine::newPixmapRenderTarget(imvu::Vec2i size)
{ {
log::info("engine: frame {}: a {}x{} pixmap render target", frames_, size.x, size.y);
return inner_->newPixmapRenderTarget(size); return inner_->newPixmapRenderTarget(size);
} }
imvu::SharedPtr<imvu::PiTextureRenderTarget> Engine::newTextureRenderTarget(imvu::Vec2i size) imvu::SharedPtr<imvu::PiTextureRenderTarget> Engine::newTextureRenderTarget(imvu::Vec2i size)
{ {
log::info("engine: frame {}: a {}x{} texture render target", frames_, size.x, size.y);
return inner_->newTextureRenderTarget(size); return inner_->newTextureRenderTarget(size);
} }
void Engine::beginRendering(imvu::PiRenderTarget* target) void Engine::beginRendering(imvu::PiRenderTarget* target)
{ {
inner_->beginRendering(target); inner_->beginRendering(target);
if (target != nullptr && foreignLogged_ < 20) {
++foreignLogged_;
log::info("engine: frame {}: drawing into a render target ({})", frames_, static_cast<void*>(target));
}
recorder_->add(Op::BeginFrame, protocol::BeginFrame{target != nullptr}); recorder_->add(Op::BeginFrame, protocol::BeginFrame{target != nullptr});
} }
@@ -241,6 +257,14 @@ void Engine::endRendering()
recorder_->add(Op::EndFrame, protocol::EndFrame{static_cast<std::uint64_t>(now.QuadPart)}); recorder_->add(Op::EndFrame, protocol::EndFrame{static_cast<std::uint64_t>(now.QuadPart)});
recorder_->flush(true); recorder_->flush(true);
++frames_; ++frames_;
if (frames_ % 300 == 0 && statsLogged_ < 6) {
++statsLogged_;
log::info("engine: last 300 frames: drawTriangles {} (and {} with buffers not ours), drawTrianglesFromMemory {} "
"(and {} without indices), point sprites {}, lines {}, 2D projections {}",
stats_.triangles, stats_.foreignTriangles, stats_.indexed, stats_.unindexed, stats_.sprites, stats_.lines,
stats_.flat);
stats_ = {};
}
} }
void Engine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions, void Engine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
@@ -248,15 +272,21 @@ void Engine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInterna
imvu::PiInternalVertexBuffer* colors) imvu::PiInternalVertexBuffer* colors)
{ {
inner_->drawTriangles(inner(indices), inner(positions), inner(normals), inner(texCoords), inner(colors)); inner_->drawTriangles(inner(indices), inner(positions), inner(normals), inner(texCoords), inner(colors));
++(id(indices) != 0 && id(positions) != 0 ? stats_.triangles : stats_.foreignTriangles);
recorder_->add(Op::DrawTriangles, recorder_->add(Op::DrawTriangles,
protocol::DrawTriangles{id(indices), id(positions), id(normals), id(texCoords), id(colors)}); protocol::DrawTriangles{id(indices), id(positions), id(normals), id(texCoords), id(colors)});
} }
void Engine::drawLine(imvu::Vec3f from, imvu::Vec3f to, imvu::Color color) { inner_->drawLine(from, to, color); } void Engine::drawLine(imvu::Vec3f from, imvu::Vec3f to, imvu::Color color)
{
inner_->drawLine(from, to, color);
++stats_.lines;
}
void Engine::drawPointSprites(const imvu::PointSpriteVector& sprites) void Engine::drawPointSprites(const imvu::PointSpriteVector& sprites)
{ {
inner_->drawPointSprites(sprites); inner_->drawPointSprites(sprites);
++stats_.sprites;
if (sprites.first == nullptr || sprites.last <= sprites.first) { if (sprites.first == nullptr || sprites.last <= sprites.first) {
return; return;
} }
@@ -275,9 +305,23 @@ void Engine::drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t in
const imvu::VertexAttributeArray<imvu::Color>& colors) const imvu::VertexAttributeArray<imvu::Color>& colors)
{ {
inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, colors); inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, colors);
if (indices == nullptr || positions.data == nullptr || vertexCount == 0) { ++(indices != nullptr ? stats_.indexed : stats_.unindexed);
if (positions.data == nullptr || vertexCount == 0) {
return; return;
} }
// Without indices: the vertices in order, as a triangle list.
std::vector<std::uint16_t> sequence;
if (indices == nullptr) {
if (vertexCount > 0xffff) {
return;
}
sequence.resize(vertexCount - vertexCount % 3);
for (std::size_t i = 0; i < sequence.size(); ++i) {
sequence[i] = static_cast<std::uint16_t>(i);
}
indices = sequence.data();
indexCount = static_cast<std::uint32_t>(sequence.size());
}
const bool hasNormals = normals.data != nullptr && normals.count >= vertexCount; const bool hasNormals = normals.data != nullptr && normals.count >= vertexCount;
const bool hasTexCoords = texCoords.data != nullptr && texCoords.count >= vertexCount; const bool hasTexCoords = texCoords.data != nullptr && texCoords.count >= vertexCount;
const bool hasColors = colors.data != nullptr && colors.count >= vertexCount; const bool hasColors = colors.data != nullptr && colors.count >= vertexCount;
+15
View File
@@ -67,6 +67,21 @@ private:
imvu::SharedPtr<imvu::PiRenderEngine> inner_; imvu::SharedPtr<imvu::PiRenderEngine> inner_;
std::shared_ptr<Recorder> recorder_; std::shared_ptr<Recorder> recorder_;
std::uint64_t frames_ = 0; std::uint64_t frames_ = 0;
int foreignLogged_ = 0; // diagnostics: textures the client renderer made, render targets
// Diagnostics: what the client draws, summed over 300 frames and logged (the first few times).
struct Stats {
std::uint32_t triangles = 0; // drawTriangles with our buffers
std::uint32_t foreignTriangles = 0; // drawTriangles with buffers that aren't ours (not drawn)
std::uint32_t indexed = 0; // drawTrianglesFromMemory with indices
std::uint32_t unindexed = 0; // drawTrianglesFromMemory without
std::uint32_t sprites = 0;
std::uint32_t lines = 0;
std::uint32_t flat = 0; // setProjection with an orthographic projection (2D)
};
Stats stats_;
int statsLogged_ = 0;
}; };
} // namespace t5de } // namespace t5de
+6 -2
View File
@@ -33,13 +33,17 @@ imvu::SharedPtr<imvu::PiRenderEngine> create_imvu_engine(imvu::SharedPtr<imvu::I
return create(std::move(window), imvu::RendererType::Direct3D); return create(std::move(window), imvu::RendererType::Direct3D);
} }
// What the renderer process draws: T5DE_RENDERER_VIEW=off, split (the default: the right half) or full. // What the renderer process draws: T5DE_RENDERER_VIEW=off, split (the default: the right half), full or compare.
// (The renderer process can also be switched while it runs: renderer/main.cpp.)
protocol::View view() protocol::View view()
{ {
wchar_t value[16] = {}; wchar_t value[16] = {};
GetEnvironmentVariableW(L"T5DE_RENDERER_VIEW", value, 16); GetEnvironmentVariableW(L"T5DE_RENDERER_VIEW", value, 16);
const std::wstring_view setting = value; const std::wstring_view setting = value;
return setting == L"off" ? protocol::View::Off : setting == L"full" ? protocol::View::Full : protocol::View::Split; return setting == L"off" ? protocol::View::Off
: setting == L"full" ? protocol::View::Full
: setting == L"compare" ? protocol::View::Compare
: protocol::View::Split;
} }
} // namespace } // namespace
+26 -1
View File
@@ -29,6 +29,31 @@ std::filesystem::path module_folder()
std::string narrow(const std::wstring& text) { return std::filesystem::path(text).string(); } std::string narrow(const std::wstring& text) { return std::filesystem::path(text).string(); }
// The renderer process runs from a copy in %TEMP%\t5de-renderer, one per version of the program (by size and
// modification time), so the program in the client's folder can be replaced (updated) while the client runs: the
// next engine runs the new one. Copies no longer running are removed. Falls back to the program itself.
std::filesystem::path running_copy(const std::filesystem::path& exe)
{
std::error_code error;
const auto size = std::filesystem::file_size(exe, error);
const auto modified = std::filesystem::last_write_time(exe, error);
if (error) {
return exe;
}
const auto folder = std::filesystem::temp_directory_path(error) / L"t5de-renderer";
std::filesystem::create_directories(folder, error);
const auto copy = folder / std::format(L"t5de-renderer-{}-{}.exe", size, modified.time_since_epoch().count());
if (!std::filesystem::exists(copy, error)) {
for (const auto& old : std::filesystem::directory_iterator(folder, error)) {
std::filesystem::remove(old.path(), error); // fails, harmlessly, for copies still running
}
if (!CopyFileW(exe.c_str(), copy.c_str(), FALSE)) {
return exe;
}
}
return copy;
}
} // namespace } // namespace
std::unique_ptr<RendererProcess> RendererProcess::start(HWND window, protocol::View view) std::unique_ptr<RendererProcess> RendererProcess::start(HWND window, protocol::View view)
@@ -64,7 +89,7 @@ std::unique_ptr<RendererProcess> RendererProcess::start(HWND window, protocol::V
// The renderer's window goes on top of the client's 3D view: keep the client's drawing out of it. // The renderer's window goes on top of the client's 3D view: keep the client's drawing out of it.
SetWindowLongW(window, GWL_STYLE, GetWindowLongW(window, GWL_STYLE) | WS_CLIPCHILDREN); SetWindowLongW(window, GWL_STYLE, GetWindowLongW(window, GWL_STYLE) | WS_CLIPCHILDREN);
const auto exe = module_folder() / L"t5de-renderer.exe"; const auto exe = running_copy(module_folder() / L"t5de-renderer.exe");
std::wstring command = std::format(L"\"{}\" --channel {} --parent {}", exe.wstring(), name, GetCurrentProcessId()); std::wstring command = std::format(L"\"{}\" --channel {} --parent {}", exe.wstring(), name, GetCurrentProcessId());
STARTUPINFOW startup{.cb = sizeof(STARTUPINFOW)}; STARTUPINFOW startup{.cb = sizeof(STARTUPINFOW)};
PROCESS_INFORMATION process{}; PROCESS_INFORMATION process{};
+46
View File
@@ -1,14 +1,56 @@
#include "resources.hpp" #include "resources.hpp"
#include <mutex>
#include <unordered_set>
namespace t5de { namespace t5de {
namespace {
// Our live resources, by address.
struct Registry {
std::mutex mutex;
std::unordered_set<const void*> objects;
};
Registry& registry()
{
static Registry instance;
return instance;
}
void add(const void* object)
{
const std::scoped_lock lock(registry().mutex);
registry().objects.insert(object);
}
void remove(const void* object)
{
const std::scoped_lock lock(registry().mutex);
registry().objects.erase(object);
}
} // namespace
bool ours(const void* object)
{
if (object == nullptr) {
return false;
}
const std::scoped_lock lock(registry().mutex);
return registry().objects.contains(object);
}
Texture::Texture(imvu::PiRenderTexture* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id) Texture::Texture(imvu::PiRenderTexture* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
: inner_(inner), recorder_(std::move(recorder)), id_(id) : inner_(inner), recorder_(std::move(recorder)), id_(id)
{ {
add(static_cast<imvu::PiRenderTexture*>(this));
} }
Texture::~Texture() Texture::~Texture()
{ {
remove(static_cast<imvu::PiRenderTexture*>(this));
delete inner_; // through its virtual destructor: IMVU's code frees it delete inner_; // through its virtual destructor: IMVU's code frees it
recorder_->destroyTexture(id_); recorder_->destroyTexture(id_);
} }
@@ -32,10 +74,12 @@ imvu::Pixmap* Texture::read(imvu::Pixmap* result) const { return inner_->read(re
IndexBuffer::IndexBuffer(imvu::PiInternalIndexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id) IndexBuffer::IndexBuffer(imvu::PiInternalIndexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
: inner_(inner), recorder_(std::move(recorder)), id_(id) : inner_(inner), recorder_(std::move(recorder)), id_(id)
{ {
add(static_cast<imvu::PiInternalIndexBuffer*>(this));
} }
IndexBuffer::~IndexBuffer() IndexBuffer::~IndexBuffer()
{ {
remove(static_cast<imvu::PiInternalIndexBuffer*>(this));
delete inner_; delete inner_;
recorder_->destroyBuffer(id_); recorder_->destroyBuffer(id_);
} }
@@ -43,10 +87,12 @@ IndexBuffer::~IndexBuffer()
VertexBuffer::VertexBuffer(imvu::PiInternalVertexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id) VertexBuffer::VertexBuffer(imvu::PiInternalVertexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
: inner_(inner), recorder_(std::move(recorder)), id_(id) : inner_(inner), recorder_(std::move(recorder)), id_(id)
{ {
add(static_cast<imvu::PiInternalVertexBuffer*>(this));
} }
VertexBuffer::~VertexBuffer() VertexBuffer::~VertexBuffer()
{ {
remove(static_cast<imvu::PiInternalVertexBuffer*>(this));
delete inner_; delete inner_;
recorder_->destroyBuffer(id_); recorder_->destroyBuffer(id_);
} }
+11 -7
View File
@@ -62,30 +62,34 @@ private:
std::uint32_t id_; std::uint32_t id_;
}; };
// The client renderer's object and our id for something the engine made; null and 0 for null. // Whether an object is one of ours (live Texture, IndexBuffer or VertexBuffer): the client can also hand the engine
// objects the client renderer made itself (render targets' textures), which must reach it unchanged.
bool ours(const void* object);
// The client renderer's object, and our id, for what the client passes in: ours unwrapped, others unchanged (id 0).
inline imvu::PiRenderTexture* inner(imvu::PiRenderTexture* texture) inline imvu::PiRenderTexture* inner(imvu::PiRenderTexture* texture)
{ {
return texture != nullptr ? static_cast<Texture*>(texture)->inner() : nullptr; return ours(texture) ? static_cast<Texture*>(texture)->inner() : texture;
} }
inline std::uint32_t id(imvu::PiRenderTexture* texture) inline std::uint32_t id(imvu::PiRenderTexture* texture)
{ {
return texture != nullptr ? static_cast<Texture*>(texture)->id() : 0; return ours(texture) ? static_cast<Texture*>(texture)->id() : 0;
} }
inline imvu::PiInternalIndexBuffer* inner(imvu::PiInternalIndexBuffer* buffer) inline imvu::PiInternalIndexBuffer* inner(imvu::PiInternalIndexBuffer* buffer)
{ {
return buffer != nullptr ? static_cast<IndexBuffer*>(buffer)->inner() : nullptr; return ours(buffer) ? static_cast<IndexBuffer*>(buffer)->inner() : buffer;
} }
inline std::uint32_t id(imvu::PiInternalIndexBuffer* buffer) inline std::uint32_t id(imvu::PiInternalIndexBuffer* buffer)
{ {
return buffer != nullptr ? static_cast<IndexBuffer*>(buffer)->id() : 0; return ours(buffer) ? static_cast<IndexBuffer*>(buffer)->id() : 0;
} }
inline imvu::PiInternalVertexBuffer* inner(imvu::PiInternalVertexBuffer* buffer) inline imvu::PiInternalVertexBuffer* inner(imvu::PiInternalVertexBuffer* buffer)
{ {
return buffer != nullptr ? static_cast<VertexBuffer*>(buffer)->inner() : nullptr; return ours(buffer) ? static_cast<VertexBuffer*>(buffer)->inner() : buffer;
} }
inline std::uint32_t id(imvu::PiInternalVertexBuffer* buffer) inline std::uint32_t id(imvu::PiInternalVertexBuffer* buffer)
{ {
return buffer != nullptr ? static_cast<VertexBuffer*>(buffer)->id() : 0; return ours(buffer) ? static_cast<VertexBuffer*>(buffer)->id() : 0;
} }
} // namespace t5de } // namespace t5de
+15 -2
View File
@@ -36,8 +36,21 @@ client exits.
## Status ## Status
The renderer process draws the client's whole scene, over the right half of the 3D view, beside the client's own The renderer process draws the client's whole scene, over the right half of the 3D view, beside the client's own
Direct3D renderer on the left (the adapter still forwards everything to it): `T5DE_RENDERER_VIEW=full` or `off` Direct3D renderer on the left (the adapter still forwards everything to it). Checked in rooms with screenshots of
changes that. Checked in rooms: the halves match, particles included. both renderers side by side (the compare view): they match, particles and the 2D overlay (name tags) included.
For comparing while the client runs (development):
- Views: `T5DE_RENDERER_VIEW` (off, split, full or compare) when the client starts, or
`%TEMP%\t5de-renderer-view.txt` holding one of those at any time. Compare draws the left half's scene over the
right half, so both renderers show the same thing side by side.
- `%TEMP%\t5de-renderer-shaders\fixed_function.hlsl`, if present, replaces the built-in shaders and is reloaded
when it changes.
- The renderer process runs from a copy (`%TEMP%\t5de-renderer`), so `t5de-renderer.exe` can be replaced while the
client runs; engines started afterwards run the new one (the room's view keeps its engine until the client
restarts).
- The adapter's log (`%TEMP%\t5de-renderer.log`) sums up what the client draws every 300 frames (the first few
times), and logs textures and render targets the client renderer made itself.
- The command stream (`include/t5de/protocol/commands.hpp`) carries state, transforms, lights, fog, textures, - The command stream (`include/t5de/protocol/commands.hpp`) carries state, transforms, lights, fog, textures,
buffers (copied when created or updated) and draws, in chunks through the channel; the adapter waits for each to buffers (copied when created or updated) and draws, in chunks through the channel; the adapter waits for each to
+9 -2
View File
@@ -71,9 +71,16 @@ virtual methods, IMVU's code only uses an engine through helpers that call its v
vertical field of view, Direct3D's 0 to 1 depth) and an orthographic one for 2D, orthonormal view rotations, vertical field of view, Direct3D's 0 to 1 depth) and an orthographic one for 2D, orthonormal view rotations,
opaque clear colors, ambient light with zero alpha, linear fog (1 to 50), scrolling texture transforms. Furniture opaque clear colors, ambient light with zero alpha, linear fog (1 to 50), scrolling texture transforms. Furniture
vertices are interleaved: positions and normals are 12-byte elements 32 bytes apart, so strides matter. vertices are interleaved: positions and normals are 12-byte elements 32 bytes apart, so strides matter.
- Projections give depth from -1 to 1, as OpenGL's do: IMVU's Direct3D renderer multiplies them by a matrix
mapping it to 0 to 1 (z' = z / 2 + w / 2) and shifting them half a pixel (-1 / width, +1 / height, for Direct3D
9's pixel centers). Without the depth mapping, everything in the nearer half of the depth range is clipped: the
2D overlay (name tags, the frame rate, the room's title) is drawn at depth -0.98.
- The 2D overlay is ordinary `drawTriangles` with an orthographic projection (a few per frame), textured with text
IMVU draws with GDI into bitmaps.
- Particles (`drawPointSprites`): a Visual C++ 2010 `std::vector` of 16-byte points (position, color), drawn as - Particles (`drawPointSprites`): a Visual C++ 2010 `std::vector` of 16-byte points (position, color), drawn as
Direct3D 9 point sprites: squares viewport height / distance pixels across (Direct3D 9's defaults clamp them to Direct3D 9 point sprites: squares viewport height / distance pixels across (IMVU sets the size, 1, and the scale
1 to 64; IMVU sets only the size, 1, and the scale factors 0, 0, 1), textured over the whole square. factors 0, 0, 1), textured over the whole square. Direct3D 9's documented 64-pixel default limit doesn't apply in
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 - 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. value (as Direct3D 9's documentation suggests for projections like IMVU's) doesn't.
- Still unknown: render targets, `CompoundRegion` (texture update - Still unknown: render targets, `CompoundRegion` (texture update
+1
View File
@@ -14,6 +14,7 @@ enum class View : std::uint32_t {
Off, // none Off, // none
Split, // the right half, to compare with the client's renderer on the left Split, // the right half, to compare with the client's renderer on the left
Full, Full,
Compare, // over the right half, the left half's scene: both renderers side by side, showing the same
}; };
// A shared memory section the adapter creates for each engine, named by the renderer process's `--channel <name>` // A shared memory section the adapter creates for each engine, named by the renderer process's `--channel <name>`
+95 -13
View File
@@ -84,15 +84,90 @@ HWND create_child_window(HWND parent)
0, 0, 1, 1, parent, nullptr, windowClass.hInstance, nullptr); 0, 0, 1, 1, parent, nullptr, windowClass.hInstance, nullptr);
} }
// Where the child window goes in the client's 3D view. // The view, as the adapter set it, or as %TEMP%\t5de-renderer-view.txt says while the client runs (off, split, full
RECT placement(HWND parent, View view) // or compare): for comparing the renderers without restarting the client. Read every second or so.
View current_view(View adapter)
{ {
RECT client{}; static View view = adapter;
GetClientRect(parent, &client); static ULONGLONG checked = 0;
if (view == View::Split) { if (GetTickCount64() - checked < 1000) {
client.left = client.right / 2; return view;
} }
return client; checked = GetTickCount64();
view = adapter;
wchar_t temp[MAX_PATH + 1];
const DWORD length = GetTempPathW(MAX_PATH + 1, temp);
if (length == 0 || length > MAX_PATH) {
return view;
}
const HANDLE file = CreateFileW((std::filesystem::path(temp) / L"t5de-renderer-view.txt").c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) {
return view;
}
char text[16] = {};
DWORD read = 0;
ReadFile(file, text, sizeof(text) - 1, &read, nullptr);
CloseHandle(file);
const std::string_view setting(text, std::strcspn(text, "\r\n "));
view = setting == "off" ? View::Off
: setting == "split" ? View::Split
: setting == "full" ? View::Full
: setting == "compare" ? View::Compare
: adapter;
return view;
}
// Shader changes to try while the client runs: %TEMP%\t5de-renderer-shaders\fixed_function.hlsl, if there is
// one, replaces the built-in shaders, and is read again when it changes (checked every second or so).
void reload_shaders(t5de::Scene& scene)
{
static ULONGLONG checked = 0;
static std::filesystem::file_time_type loaded{};
if (GetTickCount64() - checked < 1000) {
return;
}
checked = GetTickCount64();
std::error_code error;
const auto path = std::filesystem::temp_directory_path(error) / L"t5de-renderer-shaders" / L"fixed_function.hlsl";
const auto modified = std::filesystem::last_write_time(path, error);
if (error || modified == loaded) {
return;
}
const HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) {
return;
}
std::string source(static_cast<std::size_t>(std::filesystem::file_size(path, error)), ' ');
DWORD read = 0;
ReadFile(file, source.data(), static_cast<DWORD>(source.size()), &read, nullptr);
CloseHandle(file);
source.resize(read);
loaded = modified;
scene.setShaderSource(std::move(source));
info("shaders reloaded from {}", path.string());
}
// Where the child window goes in the client's 3D view, and where in the 3D view its scene starts (offset).
struct Placement {
RECT window{};
int offset = 0;
};
Placement placement(HWND parent, View view)
{
Placement place;
GetClientRect(parent, &place.window);
if (view == View::Split || view == View::Compare) {
place.window.left = place.window.right / 2;
}
place.offset = view == View::Compare ? 0 : place.window.left;
if (view == View::Off) {
place.window = {};
}
return place;
} }
} // namespace } // namespace
@@ -203,18 +278,25 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
} }
// Follow the client's 3D view's size. // Follow the client's 3D view's size.
const RECT place = placement(clientWindow, channel->view); const auto [place, offset] = placement(clientWindow, current_view(channel->view));
const int width = place.right - place.left; const int width = place.right - place.left;
const int height = place.bottom - place.top; const int height = place.bottom - place.top;
if (std::memcmp(&place, &placed, sizeof(RECT)) != 0 && width > 0 && height > 0) { if (std::memcmp(&place, &placed, sizeof(RECT)) != 0) {
SetWindowPos(window, nullptr, place.left, place.top, width, height, SWP_NOZORDER | SWP_NOACTIVATE);
swapChain->Resize(static_cast<Uint32>(width), static_cast<Uint32>(height));
placed = place; placed = place;
info("drawing {}x{} at {} in the client's 3D view", width, height, place.left); if (width > 0 && height > 0) {
SetWindowPos(window, nullptr, place.left, place.top, width, height,
SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW);
swapChain->Resize(static_cast<Uint32>(width), static_cast<Uint32>(height));
info("drawing {}x{} at {} in the client's 3D view, its scene from {}", width, height, place.left, offset);
} else {
ShowWindow(window, SW_HIDE);
info("hidden");
} }
}
reload_shaders(scene);
LARGE_INTEGER start, end; LARGE_INTEGER start, end;
QueryPerformanceCounter(&start); QueryPerformanceCounter(&start);
if (scene.execute(std::span(frame).first(complete), place.left, width > 0 && height > 0)) { if (scene.execute(std::span(frame).first(complete), offset, width > 0 && height > 0)) {
swapChain->Present(0); swapChain->Present(0);
QueryPerformanceCounter(&end); QueryPerformanceCounter(&end);
drawTime += static_cast<double>(end.QuadPart - start.QuadPart) / static_cast<double>(frequency.QuadPart); drawTime += static_cast<double>(end.QuadPart - start.QuadPart) / static_cast<double>(frequency.QuadPart);
+16 -15
View File
@@ -4,7 +4,6 @@
#include <string> #include <string>
#include "fixed_function_hlsl.hpp" #include "fixed_function_hlsl.hpp"
#include "log.hpp"
namespace t5de { namespace t5de {
@@ -56,7 +55,7 @@ constexpr std::uint32_t hasColors = 4;
} // namespace } // namespace
Scene::Scene(IRenderDevice* device, IDeviceContext* context, ISwapChain* swapChain) Scene::Scene(IRenderDevice* device, IDeviceContext* context, ISwapChain* swapChain)
: device_(device), context_(context), swapChain_(swapChain) : device_(device), context_(context), swapChain_(swapChain), shaderSource_(fixedFunctionHlsl)
{ {
BufferDesc constants; BufferDesc constants;
constants.Name = "Constants"; constants.Name = "Constants";
@@ -86,6 +85,13 @@ Scene::Scene(IRenderDevice* device, IDeviceContext* context, ISwapChain* swapCha
constants_.flags[2] = 127; // the client's alpha reference constants_.flags[2] = 127; // the client's alpha reference
} }
void Scene::setShaderSource(std::string source)
{
shaderSource_ = std::move(source);
shaders_.clear();
pipelines_.clear();
}
bool Scene::execute(std::span<const std::byte> commands, int offsetX, bool draw) bool Scene::execute(std::span<const std::byte> commands, int offsetX, bool draw)
{ {
offsetX_ = offsetX; offsetX_ = offsetX;
@@ -217,6 +223,13 @@ bool Scene::execute(std::span<const std::byte> commands, int offsetX, bool draw)
void Scene::setProjection(const protocol::SetProjection& projection) void Scene::setProjection(const protocol::SetProjection& projection)
{ {
std::memcpy(constants_.projection, projection.m, sizeof(constants_.projection)); std::memcpy(constants_.projection, projection.m, sizeof(constants_.projection));
// The client's projections give depth from -1 to 1, as OpenGL does; IMVU's Direct3D renderer maps it to 0 to 1
// (z' = z / 2 + w / 2) before setting them, and so do we. (It also shifts them half a pixel, which Direct3D 9
// needs and Direct3D 11 doesn't.)
for (int row = 0; row < 4; ++row) {
float* m = constants_.projection + row * 4;
m[2] = 0.5f * m[2] + 0.5f * m[3];
}
constants_.viewport[0] = static_cast<float>(projection.width); constants_.viewport[0] = static_cast<float>(projection.width);
constants_.viewport[1] = static_cast<float>(projection.height); constants_.viewport[1] = static_cast<float>(projection.height);
if (!onscreen_) { if (!onscreen_) {
@@ -535,18 +548,6 @@ void Scene::drawPointSprites(const protocol::Command& command)
if (!buffer || prepare(0, true) == nullptr) { if (!buffer || prepare(0, true) == nullptr) {
return; return;
} }
if (spritesLogged_ < 5) {
++spritesLogged_;
const Texture* texture = textures_.contains(state_.texture0) ? &textures_[state_.texture0] : nullptr;
log::info("particles: {}, texture {} {}x{} flags {}, blending {} mode {}, depth test {} write {}, alpha test {} "
"reference {}, lighting {}, fog {}, ambient ({:.2f} {:.2f} {:.2f}), texture transform [{} {} {} {} {} {}]",
sprites->count, state_.texture0, texture ? texture->width : 0, texture ? texture->height : 0,
texture ? texture->flags : 0, state_.blending, state_.blendMode, state_.depthTest, state_.depthWrite,
constants_.flags[1], constants_.flags[2], constants_.flags[0], constants_.fog[2], constants_.ambient[0],
constants_.ambient[1], constants_.ambient[2], constants_.texTransform0[0], constants_.texTransform0[1],
constants_.texTransform0[2], constants_.texTransform0[4], constants_.texTransform0[5],
constants_.texTransform0[6]);
}
IBuffer* buffers[] = {buffer}; IBuffer* buffers[] = {buffer};
context_->SetVertexBuffers(0, 1, buffers, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION, context_->SetVertexBuffers(0, 1, buffers, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION,
SET_VERTEX_BUFFERS_FLAG_RESET); SET_VERTEX_BUFFERS_FLAG_RESET);
@@ -565,7 +566,7 @@ const Scene::Shaders& Scene::shaders(std::uint32_t streams, std::uint32_t textur
const std::string source = std::string("#define HAS_NORMALS ") + ((streams & hasNormals) != 0 ? "1" : "0") + const std::string source = std::string("#define HAS_NORMALS ") + ((streams & hasNormals) != 0 ? "1" : "0") +
"\n#define HAS_TEX_COORDS " + ((streams & hasTexCoords) != 0 ? "1" : "0") + "\n#define HAS_TEX_COORDS " + ((streams & hasTexCoords) != 0 ? "1" : "0") +
"\n#define HAS_COLORS " + ((streams & hasColors) != 0 ? "1" : "0") + "\n#define HAS_COLORS " + ((streams & hasColors) != 0 ? "1" : "0") +
"\n#define TEXTURES " + std::to_string(textures) + "\n" + fixedFunctionHlsl; "\n#define TEXTURES " + std::to_string(textures) + "\n" + shaderSource_;
Shaders created; Shaders created;
ShaderCreateInfo info; ShaderCreateInfo info;
info.SourceLanguage = SHADER_SOURCE_LANGUAGE_HLSL; info.SourceLanguage = SHADER_SOURCE_LANGUAGE_HLSL;
+5 -1
View File
@@ -6,6 +6,7 @@
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <span> #include <span>
#include <string>
#include <unordered_map> #include <unordered_map>
#include "EngineFactoryD3D11.h" #include "EngineFactoryD3D11.h"
@@ -23,6 +24,9 @@ public:
// present). // present).
bool execute(std::span<const std::byte> commands, int offsetX, bool draw); bool execute(std::span<const std::byte> commands, int offsetX, bool draw);
// Replaces the shaders' source (the built-in one by default): for trying changes while the client runs.
void setShaderSource(std::string source);
// When the adapter sent the last frame carried out (QueryPerformanceCounter). // When the adapter sent the last frame carried out (QueryPerformanceCounter).
[[nodiscard]] std::uint64_t lastSentAt() const { return lastSentAt_; } [[nodiscard]] std::uint64_t lastSentAt() const { return lastSentAt_; }
@@ -111,13 +115,13 @@ private:
std::unordered_map<std::uint32_t, Buffer> buffers_; std::unordered_map<std::uint32_t, Buffer> buffers_;
std::unordered_map<std::uint32_t, Shaders> shaders_; std::unordered_map<std::uint32_t, Shaders> shaders_;
std::unordered_map<std::uint32_t, Pipeline> pipelines_; std::unordered_map<std::uint32_t, Pipeline> pipelines_;
std::string shaderSource_;
Constants constants_{}; Constants constants_{};
State state_; State state_;
bool onscreen_ = false; // in a frame drawn into the window bool onscreen_ = false; // in a frame drawn into the window
bool draw_ = true; bool draw_ = true;
std::uint64_t lastSentAt_ = 0; std::uint64_t lastSentAt_ = 0;
int spritesLogged_ = 0;
int offsetX_ = 0; int offsetX_ = 0;
}; };
+1 -1
View File
@@ -101,7 +101,7 @@ void vs_sprite(in SpriteInput input, out PSInput output)
{ {
float4 view = mul(mul(float4(input.Position, 1.0), g_World), g_View); float4 view = mul(mul(float4(input.Position, 1.0), g_World), g_View);
float4 clip = mul(view, g_Projection); float4 clip = mul(view, g_Projection);
float size = clamp(g_Viewport.y / max(length(view.xyz), 1e-4), 1.0, 64.0); float size = clamp(g_Viewport.y / max(length(view.xyz), 1e-4), 1.0, 2048.0);
float2 corner = float2((input.Corner & 1) != 0 ? 1.0 : -1.0, (input.Corner & 2) != 0 ? -1.0 : 1.0); float2 corner = float2((input.Corner & 1) != 0 ? 1.0 : -1.0, (input.Corner & 2) != 0 ? -1.0 : 1.0);
clip.xy += corner * size / g_Viewport.xy * clip.w; // half the size each way, in clip space clip.xy += corner * size / g_Viewport.xy * clip.w; // half the size each way, in clip space
output.Position = clip; output.Position = clip;