From 299cd8c31255d15d481973bf7cadad0eda6fa8b8 Mon Sep 17 00:00:00 2001 From: dhkatz Date: Wed, 28 Oct 2026 06:43:34 -0700 Subject: [PATCH] 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 --- adapter/engine.cpp | 12 +++ adapter/engine.hpp | 2 + adapter/plugin.cpp | 8 +- adapter/renderer_process.cpp | 27 ++++++- adapter/resources.cpp | 46 ++++++++++++ adapter/resources.hpp | 18 +++-- include/t5de/protocol/channel.hpp | 5 +- renderer/main.cpp | 108 +++++++++++++++++++++++---- renderer/scene.cpp | 34 ++++++++- renderer/scene.hpp | 5 ++ renderer/shaders/fixed_function.hlsl | 2 +- 11 files changed, 239 insertions(+), 28 deletions(-) diff --git a/adapter/engine.cpp b/adapter/engine.cpp index 21281ba..3dc9293 100644 --- a/adapter/engine.cpp +++ b/adapter/engine.cpp @@ -73,6 +73,12 @@ void Engine::setViewSpace(const imvu::Transform& 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(texture)); + } + } recorder_->add(Op::SetTextures, protocol::SetTextures{id(texture0), id(texture1)}); } @@ -219,17 +225,23 @@ imvu::PiInternalVertexBuffer* Engine::newDynamicVertexBuffer(std::uint32_t eleme imvu::SharedPtr 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 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); + if (target != nullptr && foreignLogged_ < 20) { + ++foreignLogged_; + log::info("engine: frame {}: drawing into a render target ({})", frames_, static_cast(target)); + } recorder_->add(Op::BeginFrame, protocol::BeginFrame{target != nullptr}); } diff --git a/adapter/engine.hpp b/adapter/engine.hpp index fa148b7..8c56c10 100644 --- a/adapter/engine.hpp +++ b/adapter/engine.hpp @@ -67,6 +67,8 @@ private: imvu::SharedPtr inner_; std::shared_ptr recorder_; std::uint64_t frames_ = 0; + int foreignLogged_ = 0; // diagnostics: textures the client renderer made, render targets + }; } // namespace t5de diff --git a/adapter/plugin.cpp b/adapter/plugin.cpp index ef7c83e..ce9a9a8 100644 --- a/adapter/plugin.cpp +++ b/adapter/plugin.cpp @@ -33,13 +33,17 @@ imvu::SharedPtr create_imvu_engine(imvu::SharedPtr RendererProcess::start(HWND window, protocol::View view) @@ -64,7 +89,7 @@ std::unique_ptr 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. 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()); STARTUPINFOW startup{.cb = sizeof(STARTUPINFOW)}; PROCESS_INFORMATION process{}; diff --git a/adapter/resources.cpp b/adapter/resources.cpp index 475b99f..872b48e 100644 --- a/adapter/resources.cpp +++ b/adapter/resources.cpp @@ -1,14 +1,56 @@ #include "resources.hpp" +#include +#include + namespace t5de { +namespace { + +// Our live resources, by address. +struct Registry { + std::mutex mutex; + std::unordered_set 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, std::uint32_t id) : inner_(inner), recorder_(std::move(recorder)), id_(id) { + add(static_cast(this)); } Texture::~Texture() { + remove(static_cast(this)); delete inner_; // through its virtual destructor: IMVU's code frees it 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, std::uint32_t id) : inner_(inner), recorder_(std::move(recorder)), id_(id) { + add(static_cast(this)); } IndexBuffer::~IndexBuffer() { + remove(static_cast(this)); delete inner_; recorder_->destroyBuffer(id_); } @@ -43,10 +87,12 @@ IndexBuffer::~IndexBuffer() VertexBuffer::VertexBuffer(imvu::PiInternalVertexBuffer* inner, std::shared_ptr recorder, std::uint32_t id) : inner_(inner), recorder_(std::move(recorder)), id_(id) { + add(static_cast(this)); } VertexBuffer::~VertexBuffer() { + remove(static_cast(this)); delete inner_; recorder_->destroyBuffer(id_); } diff --git a/adapter/resources.hpp b/adapter/resources.hpp index a0c5e34..6452426 100644 --- a/adapter/resources.hpp +++ b/adapter/resources.hpp @@ -62,30 +62,34 @@ private: 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) { - return texture != nullptr ? static_cast(texture)->inner() : nullptr; + return ours(texture) ? static_cast(texture)->inner() : texture; } inline std::uint32_t id(imvu::PiRenderTexture* texture) { - return texture != nullptr ? static_cast(texture)->id() : 0; + return ours(texture) ? static_cast(texture)->id() : 0; } inline imvu::PiInternalIndexBuffer* inner(imvu::PiInternalIndexBuffer* buffer) { - return buffer != nullptr ? static_cast(buffer)->inner() : nullptr; + return ours(buffer) ? static_cast(buffer)->inner() : buffer; } inline std::uint32_t id(imvu::PiInternalIndexBuffer* buffer) { - return buffer != nullptr ? static_cast(buffer)->id() : 0; + return ours(buffer) ? static_cast(buffer)->id() : 0; } inline imvu::PiInternalVertexBuffer* inner(imvu::PiInternalVertexBuffer* buffer) { - return buffer != nullptr ? static_cast(buffer)->inner() : nullptr; + return ours(buffer) ? static_cast(buffer)->inner() : buffer; } inline std::uint32_t id(imvu::PiInternalVertexBuffer* buffer) { - return buffer != nullptr ? static_cast(buffer)->id() : 0; + return ours(buffer) ? static_cast(buffer)->id() : 0; } } // namespace t5de diff --git a/include/t5de/protocol/channel.hpp b/include/t5de/protocol/channel.hpp index 8de6bd7..610a39d 100644 --- a/include/t5de/protocol/channel.hpp +++ b/include/t5de/protocol/channel.hpp @@ -11,9 +11,10 @@ inline constexpr std::uint32_t version = 2; // Which part of the client's 3D view the renderer process draws (the client's own renderer draws all of it, under). enum class View : std::uint32_t { - Off, // none - Split, // the right half, to compare with the client's renderer on the left + 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 ` diff --git a/renderer/main.cpp b/renderer/main.cpp index 8660582..3f4eed9 100644 --- a/renderer/main.cpp +++ b/renderer/main.cpp @@ -84,15 +84,90 @@ HWND create_child_window(HWND parent) 0, 0, 1, 1, parent, nullptr, windowClass.hInstance, nullptr); } -// Where the child window goes in the client's 3D view. -RECT placement(HWND parent, View view) +// 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) { - RECT client{}; - GetClientRect(parent, &client); - if (view == View::Split) { - client.left = client.right / 2; + static View view = adapter; + static ULONGLONG checked = 0; + if (GetTickCount64() - checked < 1000) { + 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::filesystem::file_size(path, error)), ' '); + DWORD read = 0; + ReadFile(file, source.data(), static_cast(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 @@ -203,18 +278,25 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) } // 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 height = place.bottom - place.top; - if (std::memcmp(&place, &placed, sizeof(RECT)) != 0 && width > 0 && height > 0) { - SetWindowPos(window, nullptr, place.left, place.top, width, height, SWP_NOZORDER | SWP_NOACTIVATE); - swapChain->Resize(static_cast(width), static_cast(height)); + if (std::memcmp(&place, &placed, sizeof(RECT)) != 0) { 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(width), static_cast(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; 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); QueryPerformanceCounter(&end); drawTime += static_cast(end.QuadPart - start.QuadPart) / static_cast(frequency.QuadPart); diff --git a/renderer/scene.cpp b/renderer/scene.cpp index b06d94c..8cec536 100644 --- a/renderer/scene.cpp +++ b/renderer/scene.cpp @@ -1,5 +1,8 @@ #include "scene.hpp" +#include +#include +#include #include #include @@ -56,7 +59,7 @@ constexpr std::uint32_t hasColors = 4; } // namespace Scene::Scene(IRenderDevice* device, IDeviceContext* context, ISwapChain* swapChain) - : device_(device), context_(context), swapChain_(swapChain) + : device_(device), context_(context), swapChain_(swapChain), shaderSource_(fixedFunctionHlsl) { BufferDesc constants; constants.Name = "Constants"; @@ -86,6 +89,13 @@ Scene::Scene(IRenderDevice* device, IDeviceContext* context, ISwapChain* swapCha 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 commands, int offsetX, bool draw) { offsetX_ = offsetX; @@ -546,6 +556,26 @@ void Scene::drawPointSprites(const protocol::Command& command) 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, @@ -565,7 +595,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") + "\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" + fixedFunctionHlsl; + "\n#define TEXTURES " + std::to_string(textures) + "\n" + shaderSource_; Shaders created; ShaderCreateInfo info; info.SourceLanguage = SHADER_SOURCE_LANGUAGE_HLSL; diff --git a/renderer/scene.hpp b/renderer/scene.hpp index ba1555b..e51fb74 100644 --- a/renderer/scene.hpp +++ b/renderer/scene.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "EngineFactoryD3D11.h" @@ -23,6 +24,9 @@ public: // present). bool execute(std::span 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_; } @@ -111,6 +115,7 @@ private: std::unordered_map buffers_; std::unordered_map shaders_; std::unordered_map pipelines_; + std::string shaderSource_; Constants constants_{}; State state_; diff --git a/renderer/shaders/fixed_function.hlsl b/renderer/shaders/fixed_function.hlsl index 38c99c4..152a5ec 100644 --- a/renderer/shaders/fixed_function.hlsl +++ b/renderer/shaders/fixed_function.hlsl @@ -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 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); clip.xy += corner * size / g_Viewport.xy * clip.w; // half the size each way, in clip space output.Position = clip;