Add a pass-through renderer plugin
t5deRenderEngine.dll implements IMVU Classic's renderer plugin interface by wrapping IMVU's own OpenGL renderer, and logs what each frame calls, to prove our declarations of the interface. Built with CMake presets for 32-bit MSVC; docs/interface.md has the findings. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
dc643d271b
16 files changed
+1037
No files matched your search
@@ -0,0 +1,4 @@
|
||||
/build/
|
||||
/out/
|
||||
/.vs/
|
||||
CMakeUserPresets.json
|
||||
@@ -0,0 +1,21 @@
|
||||
# t5de-renderer
|
||||
|
||||
A renderer plugin for IMVU Classic (see README.md and docs/interface.md). Part of T5DE; the patches and the patcher
|
||||
are in the `t5de` repository (git.t5de.dev/t5de/t5de, usually checked out beside this one).
|
||||
|
||||
## Rules
|
||||
|
||||
- Never commit IMVU's code: no decompiled or disassembled code, no client binaries, no Ghidra projects. Our own
|
||||
declarations of IMVU's interfaces (names, signatures, layouts we worked out) and notes in our own words are fine.
|
||||
- Test with a throwaway IMVU account, never a real one.
|
||||
- Commits: author "dhkatz <dev@t5de.dev>"; never the user's real name or email.
|
||||
- The user wants native code readable and auditable, built with modern CMake (targets, presets).
|
||||
|
||||
## Notes
|
||||
|
||||
- The plugin must match IMVU's ABI: 32-bit, MSVC (cl or clang-cl; MinGW's C++ ABI differs), `__thiscall`
|
||||
virtual methods in IMVU's vtable order, Boost 1.61's `shared_ptr` layout. Exceptions must not cross into IMVU's
|
||||
code (a different C runtime); the CRT is linked statically.
|
||||
- Ghidra 12.2 (Java 25) is at `%LOCALAPPDATA%\Programs\ghidra_12.2_PUBLIC`; projects in
|
||||
`%LOCALAPPDATA%\t5de-dev\ghidra`, analysed headless (`support\analyzeHeadless.bat`). The client to analyse is the
|
||||
`t5de` checkout's `.cache/<version>/install`.
|
||||
@@ -0,0 +1,47 @@
|
||||
cmake_minimum_required(VERSION 3.28)
|
||||
|
||||
project(t5de-renderer
|
||||
VERSION 0.1.0
|
||||
DESCRIPTION "A new 3D renderer for the IMVU Classic client"
|
||||
LANGUAGES CXX)
|
||||
|
||||
# The plugin is loaded into IMVU Classic, a 32-bit program built with Visual C++: it must use the same ABI.
|
||||
if(NOT MSVC) # cl or clang-cl
|
||||
message(FATAL_ERROR "Build with MSVC or clang-cl: the plugin must match IMVU Classic's Visual C++ ABI.")
|
||||
endif()
|
||||
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4)
|
||||
message(FATAL_ERROR "Build for 32-bit x86 (IMVU Classic is 32-bit): use a CMakePresets.json preset.")
|
||||
endif()
|
||||
|
||||
# The C runtime is linked statically, so the plugin needs nothing installed beside it.
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||
|
||||
# Warnings and conformance for our own targets.
|
||||
add_library(t5de_options INTERFACE)
|
||||
add_library(t5de::options ALIAS t5de_options)
|
||||
target_compile_features(t5de_options INTERFACE cxx_std_20)
|
||||
target_compile_options(t5de_options INTERFACE /W4 /permissive- /utf-8 /Zc:__cplusplus)
|
||||
target_compile_definitions(t5de_options INTERFACE WIN32_LEAN_AND_MEAN NOMINMAX UNICODE _UNICODE)
|
||||
|
||||
# Our declarations of IMVU's interfaces.
|
||||
add_library(t5de_imvu INTERFACE)
|
||||
add_library(t5de::imvu ALIAS t5de_imvu)
|
||||
target_include_directories(t5de_imvu INTERFACE
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
|
||||
target_compile_features(t5de_imvu INTERFACE cxx_std_20)
|
||||
|
||||
# The renderer plugin, t5deRenderEngine.dll.
|
||||
add_library(t5de_render_engine SHARED
|
||||
src/log.cpp
|
||||
src/passthrough_engine.cpp
|
||||
src/plugin.cpp
|
||||
src/plugin.def)
|
||||
set_target_properties(t5de_render_engine PROPERTIES OUTPUT_NAME t5deRenderEngine)
|
||||
target_link_libraries(t5de_render_engine PRIVATE t5de::imvu t5de::options)
|
||||
|
||||
install(TARGETS t5de_render_engine RUNTIME DESTINATION .)
|
||||
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"version": 6,
|
||||
"cmakeMinimumRequired": { "major": 3, "minor": 28, "patch": 0 },
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "windows-x86",
|
||||
"displayName": "Windows, 32-bit (Visual Studio Build Tools)",
|
||||
"description": "IMVU Classic is a 32-bit Visual C++ program: build for Win32 with MSVC.",
|
||||
"generator": "Visual Studio 18 2026",
|
||||
"architecture": { "value": "Win32", "strategy": "set" },
|
||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||
"installDir": "${sourceDir}/build/${presetName}/install",
|
||||
"condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{ "name": "debug", "configurePreset": "windows-x86", "configuration": "Debug" },
|
||||
{ "name": "release", "configurePreset": "windows-x86", "configuration": "Release" }
|
||||
],
|
||||
"testPresets": [
|
||||
{
|
||||
"name": "debug",
|
||||
"configurePreset": "windows-x86",
|
||||
"configuration": "Debug",
|
||||
"output": { "outputOnFailure": true }
|
||||
},
|
||||
{
|
||||
"name": "release",
|
||||
"configurePreset": "windows-x86",
|
||||
"configuration": "Release",
|
||||
"output": { "outputOnFailure": true }
|
||||
}
|
||||
],
|
||||
"workflowPresets": [
|
||||
{
|
||||
"name": "release",
|
||||
"steps": [
|
||||
{ "type": "configure", "name": "windows-x86" },
|
||||
{ "type": "build", "name": "release" },
|
||||
{ "type": "test", "name": "release" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# t5de-renderer
|
||||
|
||||
A new 3D renderer for the IMVU Classic client, as a renderer plugin the client loads like its own OpenGL renderer.
|
||||
Part of [T5DE](https://git.t5de.dev/t5de/t5de); unofficial, not affiliated with IMVU.
|
||||
|
||||
For now it's a pass-through: it wraps IMVU's OpenGL renderer and logs what each frame draws, to prove our
|
||||
declarations of IMVU's interface (`docs/interface.md`).
|
||||
|
||||
## Building
|
||||
|
||||
Needs Visual Studio Build Tools 2026 (the C++ workload) and CMake 3.28 or newer. IMVU Classic is a 32-bit
|
||||
Visual C++ program, so the plugin is built for 32-bit Windows (the `windows-x86` preset):
|
||||
|
||||
```sh
|
||||
cmake --workflow --preset release
|
||||
```
|
||||
|
||||
That configures, builds `build/windows-x86/Release/t5deRenderEngine.dll` and runs the tests. Or step by step:
|
||||
`cmake --preset windows-x86`, `cmake --build --preset debug`, `ctest --preset debug`.
|
||||
|
||||
## Trying it
|
||||
|
||||
Copy `t5deRenderEngine.dll` into a T5DE client folder, and have the client load it in place of
|
||||
`glRenderEngine.dll` (a T5DE patch, to come), then choose OpenGL in Settings > Advanced. The log is
|
||||
`%TEMP%\t5de-renderer.log`.
|
||||
@@ -0,0 +1,87 @@
|
||||
# IMVU Classic's renderer interface
|
||||
|
||||
Findings from client 554.8, from IMVU's exported symbols and Ghidra (headless). Ghidra projects hold IMVU's code:
|
||||
keep them out of this repository (`%LOCALAPPDATA%\t5de-dev\ghidra`; Ghidra also refuses paths with a dot-folder).
|
||||
Our declarations are in `include/t5de/imvu/`.
|
||||
|
||||
## How the client creates a renderer
|
||||
|
||||
Settings > Advanced > Graphics offers three renderers. The Python setting `graphicsRenderer` maps to a
|
||||
`RendererType`, passed to `SceneWindow.dll`'s `createRenderer(boost::shared_ptr<INativeWindow>, RendererType)`:
|
||||
|
||||
| Setting | Type | Renderer |
|
||||
|---|---|---|
|
||||
| Direct3D | 1 | `RenderEngineDx`, Direct3D 9, built into `SceneWindow.dll` |
|
||||
| OpenGL | 2 | a plugin, `glRenderEngine.dll` (`RenderEngineGl`), via `createDLLRenderer` |
|
||||
| Software ("Standard graphics") | other | `RenderEngineDx` on SwiftShader (`swiftshader.dll`, Direct3D 9 on the CPU) |
|
||||
|
||||
`D3D11` and `OpenGLSM2` exist only as Python enum values.
|
||||
|
||||
`createDLLRenderer(window, const wchar_t* dll)` (exported) loads a renderer plugin: `LoadLibraryW(dll)`, then the
|
||||
DLL's `isRenderEngineSupported()` (nonzero to accept), then `createRenderEngine(window)`. For type 2,
|
||||
`createRenderer` passes `L"glRenderEngine.dll"` with a single `push` of the string's address, so a T5DE patch can
|
||||
point that operand at our DLL's name in memory (after checking the instruction) to load this plugin instead.
|
||||
|
||||
The plugin's exports:
|
||||
|
||||
- `int isRenderEngineSupported()`: IMVU's OpenGL plugin returns 1.
|
||||
- `boost::shared_ptr<PiRenderEngine> createRenderEngine(boost::shared_ptr<INativeWindow> window)`: `__cdecl`; the
|
||||
result through a hidden pointer; the callee destroys the by-value argument (MSVC), releasing its reference.
|
||||
|
||||
## `PiRenderEngine`
|
||||
|
||||
A 2006-era fixed-function interface: 35 virtual methods (`__thiscall`). Names come from `RenderEngineDx`'s exported
|
||||
methods; signatures from their decorated names. `include/t5de/imvu/render_engine.hpp` declares them in vtable order.
|
||||
|
||||
- Frame: `beginRendering(PiRenderTarget*)`, `endRendering()`, `clear(flags, color)`, `resizeFrameBuffer(Vec2<int>)`,
|
||||
`enable2dFrameBuffer()`, render targets (`newPixmapRenderTarget`, `newTextureRenderTarget`, returning
|
||||
`boost::shared_ptr`s).
|
||||
- Transforms: `setProjection(Vec2<int>, const Mat4f&)`, `setViewSpace` and `setModelSpace(const Transform&)`,
|
||||
texture transforms per stage.
|
||||
- State: depth test and write, blend mode and blending, alpha test and value, back-face culling, normal inverting,
|
||||
lighting, `setLightProperties(index, const LightProperties&)`, fog, environment ambient color.
|
||||
- Resources: `newRenderTexture(Pixmap*, flags)`, index and vertex buffers.
|
||||
- Drawing: `drawTriangles` (an index buffer and four vertex buffers), `drawLine`, `drawPointSprites`,
|
||||
`drawTrianglesFromMemory` (vertex attribute arrays).
|
||||
- Two textures per draw (`setTextures(texture0, texture1)`).
|
||||
|
||||
`SceneWindow.dll` defines the base class: its constructor and destructor, defaults for the buffer methods and
|
||||
`drawTriangles` (IMVU's OpenGL plugin uses them), and an asserting `enable2dFrameBuffer`. Its fields, after the vtable
|
||||
pointer: a magic number (`0x3b913159`, checked by the destructor) and a size counter (12 bytes in all). Outside the
|
||||
virtual methods, IMVU's code only uses an engine through helpers that call its virtual methods (render states'
|
||||
`apply`, resources' `allocateInternalResource`).
|
||||
|
||||
## ABI
|
||||
|
||||
- 32-bit; virtual methods are `__thiscall`. Modern MSVC and clang-cl lay out single-inheritance vtables and pass
|
||||
arguments the same way as Visual C++ 2010, which built the client.
|
||||
- `boost::shared_ptr` (Boost 1.61): the object pointer, then the control block: a vtable (destructor, `dispose`,
|
||||
`destroy`, `get_deleter`, `get_untyped_deleter`), the use count and the weak count, changed atomically.
|
||||
`include/t5de/imvu/shared_ptr.hpp` matches it. With non-trivial copy and destruction, it's returned through a
|
||||
hidden pointer and passed on the stack, as IMVU's code expects.
|
||||
- Still to work out, for a renderer of our own: `Mat4f`, `Transform`, `LightProperties`, `TextureTransform`,
|
||||
`Pixmap` (`pixmap.dll`), the buffers' contents, `VertexAttributeArray<T>`, `std::vector<PointSprite>` as
|
||||
Visual C++ 2010 lays it out, and `RenderBlendMode`'s values.
|
||||
- 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.
|
||||
|
||||
## What a renderer can do
|
||||
|
||||
- Every draw passes through the interface between `beginRendering` and `endRendering`, so a renderer can record a
|
||||
frame and draw it however it likes: shaders emulating the fixed-function state, per-pixel lighting,
|
||||
anti-aliasing, anisotropic filtering, correct gamma and tone mapping, shadows and screen-space effects from the
|
||||
recorded frame, post-processing.
|
||||
- It receives only two textures, a few lights, fog and blend state per draw. Newer material features (normal,
|
||||
roughness, emissive maps) aren't passed: they'd need hooks where `SceneWindow.dll` loads products, and the maps
|
||||
must exist in the product data the servers send Classic (unverified).
|
||||
- Frame rate is not the renderer's: the scene advances in 33 ms ticks, and animations one frame per tick
|
||||
(`ActionInstance::step` calls `incrementActionFrame` per tick; `FrameIntervalMaker::getFrameInterval` counts the
|
||||
ticks; `runApp` waits out `zero.dll`'s `millisecondsPerFrame`). Shorter ticks speed animations up. Smooth high
|
||||
frame rates need in-between poses: hooks in `SceneWindow.dll`'s animation update.
|
||||
|
||||
## Plan
|
||||
|
||||
1. Pass-through plugin (this repository's first version): wraps IMVU's OpenGL engine and logs what each frame
|
||||
calls, proving our declarations.
|
||||
2. Work out the layouts above.
|
||||
3. A renderer reproducing the current look on Direct3D 11 or OpenGL 4, then the improvements.
|
||||
@@ -0,0 +1,115 @@
|
||||
#pragma once
|
||||
// IMVU's renderer interface, PiRenderEngine (SceneWindow.dll), declared to match its vtable: the order of the methods
|
||||
// and their parameter types are what matter. The names are IMVU's, from SceneWindow.dll's exported RenderEngineDx
|
||||
// methods; parameter names are ours, and guesses until the calls are traced. Types used only through pointers or
|
||||
// references are left incomplete until we need their layout. See docs/interface.md.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "t5de/imvu/shared_ptr.hpp"
|
||||
|
||||
namespace t5de::imvu {
|
||||
|
||||
template <class T>
|
||||
struct Vec2 {
|
||||
T x;
|
||||
T y;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
struct Vec3 {
|
||||
T x;
|
||||
T y;
|
||||
T z;
|
||||
};
|
||||
|
||||
using Vec2i = Vec2<int>;
|
||||
using Vec2f = Vec2<float>;
|
||||
using Vec3f = Vec3<float>;
|
||||
|
||||
struct Mat4f;
|
||||
struct Transform;
|
||||
struct LightProperties;
|
||||
struct TextureTransform;
|
||||
struct PointSprite;
|
||||
class Pixmap;
|
||||
class INativeWindow;
|
||||
class PiRenderTexture;
|
||||
class PiInternalIndexBuffer;
|
||||
class PiInternalVertexBuffer;
|
||||
class PiRenderTarget;
|
||||
class PiPixmapRenderTarget;
|
||||
class PiTextureRenderTarget;
|
||||
template <class T>
|
||||
class VertexAttributeArray;
|
||||
|
||||
// std::vector<PointSprite> as Visual C++ 2010 lays it out.
|
||||
struct PointSpriteVector;
|
||||
|
||||
// Values not yet known.
|
||||
enum class RenderBlendMode : int {};
|
||||
|
||||
class PiRenderEngine {
|
||||
public:
|
||||
PiRenderEngine() = default;
|
||||
PiRenderEngine(const PiRenderEngine&) = delete;
|
||||
PiRenderEngine& operator=(const PiRenderEngine&) = delete;
|
||||
|
||||
virtual ~PiRenderEngine() = default; // slot 0
|
||||
|
||||
virtual void resizeFrameBuffer(Vec2i size) = 0;
|
||||
virtual void enable2dFrameBuffer() = 0;
|
||||
virtual void clear(std::uint32_t flags, std::uint32_t color) = 0;
|
||||
virtual void setProjection(Vec2i viewport, const Mat4f& projection) = 0;
|
||||
virtual void setModelSpace(const Transform& model) = 0; // slot 5
|
||||
virtual void setViewSpace(const Transform& view) = 0;
|
||||
virtual void setTextures(PiRenderTexture* texture0, PiRenderTexture* texture1) = 0;
|
||||
virtual void setDepthTestingOn(bool on) = 0;
|
||||
virtual void setDepthWritingOn(bool on) = 0;
|
||||
virtual void setBlendMode(RenderBlendMode mode) = 0; // slot 10
|
||||
virtual void setAlphaTestingOn(bool on) = 0;
|
||||
virtual void setAlphaTestValue(float value) = 0;
|
||||
virtual void setBlendingOn(bool on) = 0;
|
||||
virtual void setBackFaceCullingOn(bool on) = 0;
|
||||
virtual void setNormalInvertingOn(bool on) = 0; // slot 15
|
||||
virtual void setLightingOn(bool on) = 0;
|
||||
virtual void setLightProperties(std::uint32_t index, const LightProperties& light) = 0;
|
||||
virtual void setFogOn(bool on) = 0;
|
||||
virtual void setFogAttributes(float start, float end, std::uint32_t color) = 0;
|
||||
virtual void setTextureTransformMatrixIdentity(std::uint32_t stage) = 0; // slot 20
|
||||
virtual void setTextureTransformMatrix(std::uint32_t stage, const TextureTransform& transform) = 0;
|
||||
virtual void setEnvironmentAmbient(std::uint32_t color) = 0;
|
||||
virtual PiRenderTexture* newRenderTexture(Pixmap* pixmap, std::uint32_t flags) = 0;
|
||||
virtual PiInternalIndexBuffer* newIndexBuffer(std::uint32_t count, const std::uint16_t* indices) = 0;
|
||||
virtual PiInternalVertexBuffer* newStaticVertexBuffer(const void* data, std::uint32_t a, std::uint32_t b,
|
||||
std::uint32_t c) = 0; // slot 25
|
||||
virtual PiInternalVertexBuffer* newDynamicVertexBuffer(std::uint32_t a, std::uint32_t b) = 0;
|
||||
virtual SharedPtr<PiPixmapRenderTarget> newPixmapRenderTarget(Vec2i size) = 0;
|
||||
virtual SharedPtr<PiTextureRenderTarget> newTextureRenderTarget(Vec2i size) = 0;
|
||||
virtual void beginRendering(PiRenderTarget* target) = 0;
|
||||
virtual void endRendering() = 0; // slot 30
|
||||
virtual void drawTriangles(PiInternalIndexBuffer* indices, PiInternalVertexBuffer* positions,
|
||||
PiInternalVertexBuffer* normals, PiInternalVertexBuffer* texCoords0,
|
||||
PiInternalVertexBuffer* texCoords1) = 0;
|
||||
virtual void drawLine(Vec3f from, Vec3f to, std::uint32_t color) = 0;
|
||||
virtual void drawPointSprites(const PointSpriteVector& sprites) = 0;
|
||||
virtual void drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount,
|
||||
const std::uint16_t* indices, const VertexAttributeArray<Vec3f>& positions,
|
||||
const VertexAttributeArray<Vec3f>& normals,
|
||||
const VertexAttributeArray<Vec2f>& texCoords,
|
||||
const VertexAttributeArray<std::uint32_t>& colors) = 0; // slot 34
|
||||
|
||||
private:
|
||||
// PiRenderEngine's own fields, after the vtable pointer: a magic number IMVU's destructor checks, and a size
|
||||
// counter it reports from.
|
||||
std::uint32_t magic_ = 0x3b913159;
|
||||
std::uint32_t size_ = 0;
|
||||
};
|
||||
|
||||
static_assert(sizeof(PiRenderEngine) == 12, "vtable pointer, magic, size");
|
||||
|
||||
// What SceneWindow.dll's createDLLRenderer looks up in a renderer plugin.
|
||||
using IsRenderEngineSupported = int(__cdecl*)();
|
||||
using CreateRenderEngine = SharedPtr<PiRenderEngine>(__cdecl*)(SharedPtr<INativeWindow> window);
|
||||
|
||||
} // namespace t5de::imvu
|
||||
@@ -0,0 +1,129 @@
|
||||
#pragma once
|
||||
// boost::shared_ptr as IMVU's DLLs lay it out (Boost 1.61, Visual C++ 2010, 32-bit): the object pointer, then a
|
||||
// pointer to the control block. Shared pointers cross the plugin boundary in both directions, so ours must be
|
||||
// interchangeable with theirs: same layout, same control block vtable, same atomic counts.
|
||||
|
||||
#include <intrin.h>
|
||||
|
||||
#include <memory>
|
||||
#include <typeinfo>
|
||||
#include <utility>
|
||||
|
||||
namespace t5de::imvu {
|
||||
|
||||
// boost::detail::sp_counted_base: its virtual methods, in this order, then the use and weak counts.
|
||||
class CountedBase {
|
||||
public:
|
||||
CountedBase() = default;
|
||||
CountedBase(const CountedBase&) = delete;
|
||||
CountedBase& operator=(const CountedBase&) = delete;
|
||||
|
||||
virtual ~CountedBase() = default;
|
||||
// Destroys the object; called when the use count reaches 0.
|
||||
virtual void dispose() noexcept = 0;
|
||||
// Destroys the control block; called when the weak count reaches 0.
|
||||
virtual void destroy() noexcept { delete this; }
|
||||
virtual void* get_deleter(const std::type_info&) noexcept { return nullptr; }
|
||||
virtual void* get_untyped_deleter() noexcept { return nullptr; }
|
||||
|
||||
void add_ref() noexcept { _InterlockedIncrement(&use_); }
|
||||
|
||||
void release() noexcept
|
||||
{
|
||||
if (_InterlockedDecrement(&use_) == 0) {
|
||||
dispose();
|
||||
weak_release();
|
||||
}
|
||||
}
|
||||
|
||||
void weak_release() noexcept
|
||||
{
|
||||
if (_InterlockedDecrement(&weak_) == 0) {
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] long use_count() const noexcept { return use_; }
|
||||
|
||||
private:
|
||||
long use_ = 1;
|
||||
long weak_ = 1;
|
||||
};
|
||||
|
||||
static_assert(sizeof(CountedBase) == 12, "vtable pointer, use count, weak count");
|
||||
|
||||
// A control block owning a heap object, deleted with delete (boost's sp_counted_impl_p).
|
||||
template <class T>
|
||||
class CountedPointer final : public CountedBase {
|
||||
public:
|
||||
explicit CountedPointer(T* object) noexcept : object_(object) {}
|
||||
void dispose() noexcept override { delete object_; }
|
||||
|
||||
private:
|
||||
T* object_;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
class SharedPtr {
|
||||
public:
|
||||
SharedPtr() noexcept = default;
|
||||
|
||||
explicit SharedPtr(std::unique_ptr<T> object) : object_(object.get())
|
||||
{
|
||||
if (object_ != nullptr) {
|
||||
counts_ = new CountedPointer<T>(object_);
|
||||
object.release();
|
||||
}
|
||||
}
|
||||
|
||||
// Shares ownership of a derived object (the counts stay with the object's own control block).
|
||||
template <class U>
|
||||
requires std::is_convertible_v<U*, T*>
|
||||
SharedPtr(SharedPtr<U> other) noexcept // NOLINT(google-explicit-constructor): like boost's
|
||||
: object_(std::exchange(other.object_, nullptr)), counts_(std::exchange(other.counts_, nullptr))
|
||||
{
|
||||
}
|
||||
|
||||
SharedPtr(const SharedPtr& other) noexcept : object_(other.object_), counts_(other.counts_)
|
||||
{
|
||||
if (counts_ != nullptr) {
|
||||
counts_->add_ref();
|
||||
}
|
||||
}
|
||||
|
||||
SharedPtr(SharedPtr&& other) noexcept
|
||||
: object_(std::exchange(other.object_, nullptr)), counts_(std::exchange(other.counts_, nullptr))
|
||||
{
|
||||
}
|
||||
|
||||
SharedPtr& operator=(SharedPtr other) noexcept
|
||||
{
|
||||
std::swap(object_, other.object_);
|
||||
std::swap(counts_, other.counts_);
|
||||
return *this;
|
||||
}
|
||||
|
||||
~SharedPtr()
|
||||
{
|
||||
if (counts_ != nullptr) {
|
||||
counts_->release();
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] T* get() const noexcept { return object_; }
|
||||
T* operator->() const noexcept { return object_; }
|
||||
T& operator*() const noexcept { return *object_; }
|
||||
explicit operator bool() const noexcept { return object_ != nullptr; }
|
||||
[[nodiscard]] long use_count() const noexcept { return counts_ != nullptr ? counts_->use_count() : 0; }
|
||||
|
||||
private:
|
||||
template <class U>
|
||||
friend class SharedPtr;
|
||||
|
||||
T* object_ = nullptr;
|
||||
CountedBase* counts_ = nullptr;
|
||||
};
|
||||
|
||||
static_assert(sizeof(SharedPtr<int>) == 8, "object pointer, control block pointer");
|
||||
|
||||
} // namespace t5de::imvu
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
#include "log.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
|
||||
namespace t5de::log {
|
||||
|
||||
namespace {
|
||||
|
||||
std::FILE* open()
|
||||
{
|
||||
wchar_t temp[MAX_PATH + 1];
|
||||
const DWORD length = GetTempPathW(MAX_PATH + 1, temp);
|
||||
if (length == 0 || length > MAX_PATH) {
|
||||
return nullptr;
|
||||
}
|
||||
std::FILE* file = nullptr;
|
||||
_wfopen_s(&file, (std::filesystem::path(temp) / L"t5de-renderer.log").c_str(), L"w");
|
||||
return file;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void write(std::string_view line)
|
||||
{
|
||||
static std::mutex mutex;
|
||||
static std::FILE* file = open();
|
||||
const std::scoped_lock lock(mutex);
|
||||
if (file != nullptr) {
|
||||
std::fwrite(line.data(), 1, line.size(), file);
|
||||
std::fputc('\n', file);
|
||||
std::fflush(file);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace t5de::log
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
// The renderer's log: %TEMP%\t5de-renderer.log, replaced each time the client starts.
|
||||
|
||||
#include <format>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace t5de::log {
|
||||
|
||||
void write(std::string_view line);
|
||||
|
||||
template <class... Args>
|
||||
void info(std::format_string<Args...> format, Args&&... args)
|
||||
{
|
||||
write(std::format(format, std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
} // namespace t5de::log
|
||||
@@ -0,0 +1,290 @@
|
||||
#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
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
// A renderer that forwards every call to another one (IMVU's OpenGL 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
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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 OpenGL engine, wrapped.
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
|
||||
#include "log.hpp"
|
||||
#include "passthrough_engine.hpp"
|
||||
#include "t5de/imvu/render_engine.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace t5de;
|
||||
|
||||
// The folder this DLL is in: the client's program folder.
|
||||
std::filesystem::path program_folder()
|
||||
{
|
||||
HMODULE self = nullptr;
|
||||
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(&program_folder), &self);
|
||||
wchar_t path[MAX_PATH];
|
||||
GetModuleFileNameW(self, path, MAX_PATH);
|
||||
return std::filesystem::path(path).parent_path();
|
||||
}
|
||||
|
||||
// IMVU's OpenGL engine, from the plugin in the program folder (never unloaded: its engines use its code).
|
||||
imvu::SharedPtr<imvu::PiRenderEngine> create_imvu_engine(imvu::SharedPtr<imvu::INativeWindow> window)
|
||||
{
|
||||
const auto dll = program_folder() / L"glRenderEngine.dll";
|
||||
HMODULE module = LoadLibraryW(dll.c_str());
|
||||
if (module == nullptr) {
|
||||
log::info("can't load {}: error {}", dll.string(), GetLastError());
|
||||
return {};
|
||||
}
|
||||
auto supported = reinterpret_cast<imvu::IsRenderEngineSupported>(GetProcAddress(module, "isRenderEngineSupported"));
|
||||
auto create = reinterpret_cast<imvu::CreateRenderEngine>(GetProcAddress(module, "createRenderEngine"));
|
||||
if (supported == nullptr || create == nullptr || supported() == 0) {
|
||||
log::info("{} isn't a usable renderer", dll.string());
|
||||
return {};
|
||||
}
|
||||
return create(std::move(window));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int __cdecl isRenderEngineSupported() { return 1; }
|
||||
|
||||
// Returns a C++ object from a C-linkage function: fine here, the caller is C++ built for the same ABI.
|
||||
#pragma warning(suppress : 4190)
|
||||
extern "C" imvu::SharedPtr<imvu::PiRenderEngine> __cdecl createRenderEngine(imvu::SharedPtr<imvu::INativeWindow> window)
|
||||
{
|
||||
try {
|
||||
log::info("createRenderEngine: window {}", static_cast<void*>(window.get()));
|
||||
auto inner = create_imvu_engine(std::move(window));
|
||||
if (!inner) {
|
||||
return {};
|
||||
}
|
||||
return imvu::SharedPtr<imvu::PiRenderEngine>(std::make_unique<PassthroughEngine>(std::move(inner)));
|
||||
} catch (const std::exception& e) {
|
||||
// Exceptions mustn't cross into IMVU's code (a different C++ runtime).
|
||||
log::info("createRenderEngine failed: {}", e.what());
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
; The names SceneWindow.dll's createDLLRenderer looks up, without C++ or __cdecl decoration.
|
||||
LIBRARY t5deRenderEngine
|
||||
EXPORTS
|
||||
isRenderEngineSupported
|
||||
createRenderEngine
|
||||
@@ -0,0 +1,3 @@
|
||||
add_executable(shared_ptr_test shared_ptr_test.cpp)
|
||||
target_link_libraries(shared_ptr_test PRIVATE t5de::imvu t5de::options)
|
||||
add_test(NAME shared_ptr COMMAND shared_ptr_test)
|
||||
@@ -0,0 +1,74 @@
|
||||
// SharedPtr's reference counting, and its layout: IMVU's DLLs read and write these fields directly.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
|
||||
#include "t5de/imvu/shared_ptr.hpp"
|
||||
|
||||
using t5de::imvu::SharedPtr;
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
|
||||
void check(bool ok, const char* what)
|
||||
{
|
||||
if (!ok) {
|
||||
std::printf("FAIL: %s\n", what);
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
struct Base {
|
||||
virtual ~Base() = default;
|
||||
};
|
||||
|
||||
struct Tracked : Base {
|
||||
explicit Tracked(int& alive) : alive_(alive) { ++alive_; }
|
||||
~Tracked() override { --alive_; }
|
||||
int& alive_;
|
||||
};
|
||||
|
||||
// What boost's control block looks like from IMVU's side: a vtable pointer, then the counts.
|
||||
struct BoostCounts {
|
||||
void* vtable;
|
||||
long use;
|
||||
long weak;
|
||||
};
|
||||
|
||||
const BoostCounts& counts_of(const void* shared)
|
||||
{
|
||||
return **static_cast<BoostCounts* const*>(static_cast<const void*>(static_cast<const char*>(shared) + 4));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
int alive = 0;
|
||||
{
|
||||
SharedPtr<Tracked> a(std::make_unique<Tracked>(alive));
|
||||
check(alive == 1 && a.use_count() == 1, "created");
|
||||
check(counts_of(&a).use == 1 && counts_of(&a).weak == 1, "boost layout: counts after the vtable pointer");
|
||||
{
|
||||
SharedPtr<Tracked> b = a;
|
||||
check(a.use_count() == 2 && counts_of(&a).use == 2, "copy shares");
|
||||
SharedPtr<Base> base = b;
|
||||
check(a.use_count() == 3, "converted copy shares");
|
||||
SharedPtr<Base> moved = std::move(base);
|
||||
check(a.use_count() == 3 && !base, "move transfers");
|
||||
}
|
||||
check(alive == 1 && a.use_count() == 1, "copies released");
|
||||
}
|
||||
check(alive == 0, "last owner deletes");
|
||||
|
||||
SharedPtr<Tracked> empty;
|
||||
check(!empty && empty.use_count() == 0, "empty");
|
||||
check(sizeof(SharedPtr<Tracked>) == 8, "two pointers");
|
||||
|
||||
if (failures == 0) {
|
||||
std::printf("ok\n");
|
||||
}
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in new issue
Block a user