From 3cdda4c7edc16d16cc148328f32e91a0c4ce160d Mon Sep 17 00:00:00 2001 From: dhkatz Date: Wed, 28 Oct 2026 07:01:47 -0700 Subject: [PATCH] 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 --- adapter/engine.cpp | 36 ++++++++++++++++++++++++++++++++++-- adapter/engine.hpp | 13 +++++++++++++ docs/architecture.md | 17 +++++++++++++++-- docs/interface.md | 11 +++++++++-- renderer/scene.cpp | 43 +++++++------------------------------------ renderer/scene.hpp | 1 - 6 files changed, 78 insertions(+), 43 deletions(-) diff --git a/adapter/engine.cpp b/adapter/engine.cpp index 3dc9293..5f8c524 100644 --- a/adapter/engine.cpp +++ b/adapter/engine.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "log.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) { 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); @@ -253,6 +257,14 @@ void Engine::endRendering() recorder_->add(Op::EndFrame, protocol::EndFrame{static_cast(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, @@ -260,15 +272,21 @@ void Engine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInterna imvu::PiInternalVertexBuffer* 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, 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) { inner_->drawPointSprites(sprites); + ++stats_.sprites; if (sprites.first == nullptr || sprites.last <= sprites.first) { return; } @@ -287,9 +305,23 @@ void Engine::drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t in const imvu::VertexAttributeArray& 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; } + // Without indices: the vertices in order, as a triangle list. + std::vector 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(i); + } + indices = sequence.data(); + indexCount = static_cast(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; diff --git a/adapter/engine.hpp b/adapter/engine.hpp index 8c56c10..e186695 100644 --- a/adapter/engine.hpp +++ b/adapter/engine.hpp @@ -69,6 +69,19 @@ private: 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 diff --git a/docs/architecture.md b/docs/architecture.md index 9159231..21d2a0d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -36,8 +36,21 @@ client exits. ## Status 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` -changes that. Checked in rooms: the halves match, particles included. +Direct3D renderer on the left (the adapter still forwards everything to it). 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 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 diff --git a/docs/interface.md b/docs/interface.md index 2e1bc28..0b33705 100644 --- a/docs/interface.md +++ b/docs/interface.md @@ -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, 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 (Direct3D 9's defaults clamp them to - 1 to 64; IMVU sets only the size, 1, and the scale factors 0, 0, 1), textured over the whole square. + 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 diff --git a/renderer/scene.cpp b/renderer/scene.cpp index 8cec536..3bc4344 100644 --- a/renderer/scene.cpp +++ b/renderer/scene.cpp @@ -1,13 +1,9 @@ #include "scene.hpp" -#include -#include -#include #include #include #include "fixed_function_hlsl.hpp" -#include "log.hpp" namespace t5de { @@ -227,6 +223,13 @@ bool Scene::execute(std::span commands, int offsetX, bool draw) 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(projection.width); constants_.viewport[1] = static_cast(projection.height); if (!onscreen_) { @@ -545,38 +548,6 @@ void Scene::drawPointSprites(const protocol::Command& command) if (!buffer || prepare(0, true) == nullptr) { 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]); - // The first sprite: where it is, seen from the camera, and how big it's drawn. - float point[4] = {0, 0, 0, 1}; - std::uint32_t color = 0; - std::memcpy(point, payload.data(), 12); - std::memcpy(&color, payload.data() + 12, 4); - const auto transform = [](const float (&p)[4], const float (&m)[16]) { - std::array out{}; - for (int column = 0; column < 4; ++column) { - out[column] = p[0] * m[column] + p[1] * m[4 + column] + p[2] * m[8 + column] + p[3] * m[12 + column]; - } - return out; - }; - const auto world = transform(point, constants_.world); - const float worldPoint[4] = {world[0], world[1], world[2], world[3]}; - const auto view = transform(worldPoint, constants_.view); - const float distance = std::sqrt(view[0] * view[0] + view[1] * view[1] + view[2] * view[2]); - log::info(" first sprite at ({:.2f} {:.2f} {:.2f}), color {:08x}, from the camera ({:.2f} {:.2f} {:.2f}): " - "distance {:.2f}, {:.1f} pixels across (viewport height {})", - point[0], point[1], point[2], color, view[0], view[1], view[2], distance, - std::clamp(constants_.viewport[1] / std::max(distance, 1e-4f), 1.0f, 2048.0f), constants_.viewport[1]); - } IBuffer* buffers[] = {buffer}; context_->SetVertexBuffers(0, 1, buffers, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION, SET_VERTEX_BUFFERS_FLAG_RESET); diff --git a/renderer/scene.hpp b/renderer/scene.hpp index e51fb74..d4be637 100644 --- a/renderer/scene.hpp +++ b/renderer/scene.hpp @@ -122,7 +122,6 @@ private: bool onscreen_ = false; // in a frame drawn into the window bool draw_ = true; std::uint64_t lastSentAt_ = 0; - int spritesLogged_ = 0; int offsetX_ = 0; };