Draw the client's scene in the renderer process

The adapter records everything the client draws (state, transforms, lights, fog, textures and buffers) as a command stream, sent in chunks through the shared memory channel, while IMVU's Direct3D renderer still draws the whole view. The renderer process draws it with Diligent, with shaders reproducing IMVU's Direct3D 9 fixed-function setup, over the right half of the 3D view to compare. It skips to the newest frame when it falls behind, and logs its errors and frame times.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-28 05:52:15 -07:00
1 parent 5c35c8b63a
commit 42fc55cb91
23 files changed
+2121 -472

No files matched your search

+5 -2
View File
@@ -1,11 +1,14 @@
# t5deRenderEngine.dll, the adapter: a renderer plugin for IMVU Classic (32-bit).
add_library(t5de_render_engine SHARED
engine.cpp
log.cpp
passthrough_engine.cpp
pixmap.cpp
plugin.cpp
plugin.def
renderer_process.cpp)
recorder.cpp
renderer_process.cpp
resources.cpp)
set_target_properties(t5de_render_engine PROPERTIES OUTPUT_NAME t5deRenderEngine)
target_link_libraries(t5de_render_engine PRIVATE t5de::imvu t5de::protocol t5de::options)
+296
View File
@@ -0,0 +1,296 @@
#include "engine.hpp"
#include <bit>
#include <cmath>
#include <cstring>
#include "log.hpp"
#include "resources.hpp"
namespace t5de {
using protocol::Op;
using protocol::State;
namespace {
protocol::SetTransform transform_command(const imvu::Transform& t)
{
protocol::SetTransform command{};
std::memcpy(command.m, &t.linear[0][0], sizeof(float) * 9);
command.m[9] = t.translation.x;
command.m[10] = t.translation.y;
command.m[11] = t.translation.z;
return command;
}
} // namespace
Engine::Engine(imvu::SharedPtr<imvu::PiRenderEngine> inner, std::shared_ptr<Recorder> recorder)
: inner_(std::move(inner)), recorder_(std::move(recorder))
{
}
Engine::~Engine() { log::info("engine: destroyed after {} frames", frames_); }
void Engine::setState(State state, std::uint32_t value)
{
recorder_->add(Op::SetState, protocol::SetState{state, value});
}
void Engine::resizeFrameBuffer(imvu::Vec2i size) { inner_->resizeFrameBuffer(size); }
void Engine::enable2dFrameBuffer() { inner_->enable2dFrameBuffer(); }
void Engine::clear(std::uint32_t flags, imvu::Color color)
{
inner_->clear(flags, color);
recorder_->add(Op::Clear, protocol::Clear{flags, color});
}
void Engine::setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection)
{
inner_->setProjection(viewport, projection);
protocol::SetProjection command{viewport.x, viewport.y, {}};
std::memcpy(command.m, &projection.m[0][0], sizeof(command.m));
recorder_->add(Op::SetProjection, command);
}
void Engine::setModelSpace(const imvu::Transform& model)
{
inner_->setModelSpace(model);
recorder_->add(Op::SetModel, transform_command(model));
}
void Engine::setViewSpace(const imvu::Transform& view)
{
inner_->setViewSpace(view);
recorder_->add(Op::SetView, transform_command(view));
}
void Engine::setTextures(imvu::PiRenderTexture* texture0, imvu::PiRenderTexture* texture1)
{
inner_->setTextures(inner(texture0), inner(texture1));
recorder_->add(Op::SetTextures, protocol::SetTextures{id(texture0), id(texture1)});
}
void Engine::setDepthTestingOn(bool on)
{
inner_->setDepthTestingOn(on);
setState(State::DepthTest, on);
}
void Engine::setDepthWritingOn(bool on)
{
inner_->setDepthWritingOn(on);
setState(State::DepthWrite, on);
}
void Engine::setBlendMode(imvu::RenderBlendMode mode)
{
inner_->setBlendMode(mode);
setState(State::BlendMode, static_cast<std::uint32_t>(mode));
}
void Engine::setAlphaTestingOn(bool on)
{
inner_->setAlphaTestingOn(on);
setState(State::AlphaTest, on);
}
void Engine::setAlphaTestValue(float value)
{
inner_->setAlphaTestValue(value);
setState(State::AlphaReference, static_cast<std::uint32_t>(std::lround(value * 255.0f)));
}
void Engine::setBlendingOn(bool on)
{
inner_->setBlendingOn(on);
setState(State::Blending, on);
}
void Engine::setBackFaceCullingOn(bool on)
{
inner_->setBackFaceCullingOn(on);
setState(State::Culling, on);
}
void Engine::setNormalInvertingOn(bool on)
{
inner_->setNormalInvertingOn(on);
setState(State::NormalInverting, on);
}
void Engine::setLightingOn(bool on)
{
inner_->setLightingOn(on);
setState(State::Lighting, on);
}
void Engine::setLightProperties(std::uint32_t index, const imvu::LightProperties& light)
{
inner_->setLightProperties(index, light);
protocol::SetLight command{index, light.enabled, light.diffuse,
light.type == imvu::LightType::Directional,
{light.vector.x, light.vector.y, light.vector.z}};
// IMVU's Direct3D renderer shines a directional light along -vector, along -x for a zero vector.
if (command.directional && light.vector.x == 0 && light.vector.y == 0 && light.vector.z == 0) {
command.vector[0] = 1;
}
recorder_->add(Op::SetLight, command);
}
void Engine::setFogOn(bool on)
{
inner_->setFogOn(on);
setState(State::Fog, on);
}
void Engine::setFogAttributes(float start, float end, imvu::Color color)
{
inner_->setFogAttributes(start, end, color);
recorder_->add(Op::SetFog, protocol::SetFog{start, end, color});
}
void Engine::setTextureTransformMatrixIdentity(std::uint32_t stage)
{
inner_->setTextureTransformMatrixIdentity(stage);
recorder_->add(Op::SetTextureTransform, protocol::SetTextureTransform{stage, 1, {1, 0, 0, 1, 0, 0}});
}
void Engine::setTextureTransformMatrix(std::uint32_t stage, const imvu::TextureTransform& transform)
{
inner_->setTextureTransformMatrix(stage, transform);
protocol::SetTextureTransform command{stage, 0, {}};
std::memcpy(command.m, transform.m, sizeof(command.m));
recorder_->add(Op::SetTextureTransform, command);
}
void Engine::setEnvironmentAmbient(imvu::Color color)
{
inner_->setEnvironmentAmbient(color);
setState(State::Ambient, color);
}
imvu::PiRenderTexture* Engine::newRenderTexture(const imvu::Pixmap& pixmap, std::uint32_t flags)
{
imvu::PiRenderTexture* texture = inner_->newRenderTexture(pixmap, flags);
if (texture == nullptr) {
return nullptr;
}
return new Texture(texture, recorder_, recorder_->createTexture(pixmap, flags));
}
imvu::PiInternalIndexBuffer* Engine::newIndexBuffer(std::uint32_t count, const std::uint16_t* indices)
{
imvu::PiInternalIndexBuffer* buffer = inner_->newIndexBuffer(count, indices);
if (buffer == nullptr) {
return nullptr;
}
return new IndexBuffer(buffer, recorder_,
recorder_->createBuffer(protocol::BufferKind::Index, indices, 2, 2, count));
}
imvu::PiInternalVertexBuffer* Engine::newStaticVertexBuffer(const void* data, std::uint32_t stride,
std::uint32_t elementSize, std::uint32_t elementCount)
{
imvu::PiInternalVertexBuffer* buffer = inner_->newStaticVertexBuffer(data, stride, elementSize, elementCount);
if (buffer == nullptr) {
return nullptr;
}
return new VertexBuffer(
buffer, recorder_,
recorder_->createBuffer(protocol::BufferKind::Vertex, data, stride, elementSize, elementCount));
}
imvu::PiInternalVertexBuffer* Engine::newDynamicVertexBuffer(std::uint32_t elementSize, std::uint32_t elementCount)
{
imvu::PiInternalVertexBuffer* buffer = inner_->newDynamicVertexBuffer(elementSize, elementCount);
if (buffer == nullptr) {
return nullptr;
}
return new VertexBuffer(
buffer, recorder_,
recorder_->createBuffer(protocol::BufferKind::Vertex, nullptr, 0, elementSize, elementCount));
}
imvu::SharedPtr<imvu::PiPixmapRenderTarget> Engine::newPixmapRenderTarget(imvu::Vec2i size)
{
return inner_->newPixmapRenderTarget(size);
}
imvu::SharedPtr<imvu::PiTextureRenderTarget> Engine::newTextureRenderTarget(imvu::Vec2i size)
{
return inner_->newTextureRenderTarget(size);
}
void Engine::beginRendering(imvu::PiRenderTarget* target)
{
inner_->beginRendering(target);
recorder_->add(Op::BeginFrame, protocol::BeginFrame{target != nullptr});
}
void Engine::endRendering()
{
inner_->endRendering();
recorder_->add(Op::EndFrame, protocol::EndFrame{});
recorder_->flush(true);
++frames_;
}
void Engine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
imvu::PiInternalVertexBuffer* colors)
{
inner_->drawTriangles(inner(indices), inner(positions), inner(normals), inner(texCoords), inner(colors));
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::drawPointSprites(const imvu::PointSpriteVector& sprites) { inner_->drawPointSprites(sprites); }
void Engine::drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount,
const std::uint16_t* indices,
const imvu::VertexAttributeArray<imvu::Vec3f>& positions,
const imvu::VertexAttributeArray<imvu::Vec3f>& normals,
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
const imvu::VertexAttributeArray<imvu::Color>& colors)
{
inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, colors);
if (indices == nullptr || positions.data == nullptr || vertexCount == 0) {
return;
}
const bool hasNormals = normals.data != nullptr && normals.count >= vertexCount;
const bool hasTexCoords = texCoords.data != nullptr && texCoords.count >= vertexCount;
const bool hasColors = colors.data != nullptr && colors.count >= vertexCount;
const std::size_t indexBytes = protocol::padded(std::size_t{indexCount} * 2);
const std::size_t size = indexBytes + std::size_t{vertexCount} * (12 + (hasNormals ? 12 : 0) +
(hasTexCoords ? 8 : 0) + (hasColors ? 4 : 0));
std::byte* payload = recorder_->add(Op::DrawArrays,
protocol::DrawArrays{vertexCount, indexCount, hasNormals, hasTexCoords,
hasColors},
size);
if (payload == nullptr) {
return;
}
std::memcpy(payload, indices, std::size_t{indexCount} * 2);
std::byte* next = payload + indexBytes;
pack(next, positions.data, positions.stride, 12, vertexCount);
next += std::size_t{vertexCount} * 12;
if (hasNormals) {
pack(next, normals.data, normals.stride, 12, vertexCount);
next += std::size_t{vertexCount} * 12;
}
if (hasTexCoords) {
pack(next, texCoords.data, texCoords.stride, 8, vertexCount);
next += std::size_t{vertexCount} * 8;
}
if (hasColors) {
pack(next, colors.data, colors.stride, 4, vertexCount);
}
}
} // namespace t5de
@@ -1,21 +1,21 @@
#pragma once
// A renderer that forwards every call to another one (IMVU's Direct3D engine) and logs what a frame is made of: proof
// that our declaration of the interface matches IMVU's, and a way to see how the client draws.
// The adapter's engine: what the client draws with. Everything goes to the client's own Direct3D renderer, which
// still draws the whole 3D view, and is recorded for the renderer process, which draws its part on top (the right
// half for now, to compare). If the renderer process fails, the client's renderer carries on alone.
#include <array>
#include <cstdint>
#include <memory>
#include "renderer_process.hpp"
#include "recorder.hpp"
#include "t5de/imvu/render_engine.hpp"
namespace t5de {
class PassthroughEngine final : public imvu::PiRenderEngine {
class Engine final : public imvu::PiRenderEngine {
public:
// renderer may be null.
PassthroughEngine(imvu::SharedPtr<imvu::PiRenderEngine> inner, std::unique_ptr<RendererProcess> renderer);
~PassthroughEngine() override;
// recorder may be inactive (no renderer process).
Engine(imvu::SharedPtr<imvu::PiRenderEngine> inner, std::shared_ptr<Recorder> recorder);
~Engine() override;
void resizeFrameBuffer(imvu::Vec2i size) override;
void enable2dFrameBuffer() override;
@@ -53,7 +53,7 @@ public:
void drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
imvu::PiInternalVertexBuffer* colors) override;
void drawLine(imvu::Vec3f from, imvu::Vec3f to, std::uint32_t color) override;
void drawLine(imvu::Vec3f from, imvu::Vec3f to, imvu::Color color) override;
void drawPointSprites(const imvu::PointSpriteVector& sprites) override;
void drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount, const std::uint16_t* indices,
const imvu::VertexAttributeArray<imvu::Vec3f>& positions,
@@ -61,15 +61,11 @@ public:
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
const imvu::VertexAttributeArray<imvu::Color>& 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);
void setState(protocol::State state, std::uint32_t value);
imvu::SharedPtr<imvu::PiRenderEngine> inner_;
std::unique_ptr<RendererProcess> renderer_;
std::array<std::uint32_t, slots> calls_{};
std::shared_ptr<Recorder> recorder_;
std::uint64_t frames_ = 0;
};
-395
View File
@@ -1,395 +0,0 @@
#include "passthrough_engine.hpp"
#include <windows.h>
#include <atomic>
#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; }
// Whether to log a call's arguments: the first few calls of each method, in the whole process. Enough to check that
// our layouts of IMVU's types read sensible values.
bool sampled(std::size_t slot)
{
static std::array<std::atomic<int>, PassthroughEngine::slots> calls{};
return calls[slot]++ < 3;
}
std::string text(const imvu::Vec3f& v) { return std::format("({:.3f}, {:.3f}, {:.3f})", v.x, v.y, v.z); }
std::string text(const imvu::Transform& t)
{
const auto& m = t.linear;
return std::format("[{:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f}] + {}", m[0][0], m[0][1],
m[0][2], m[1][0], m[1][1], m[1][2], m[2][0], m[2][1], m[2][2], text(t.translation));
}
// A pixmap's size, from pixmap.dll's exported Pixmap::width and Pixmap::height.
imvu::Vec2i pixmap_size(const imvu::Pixmap& pixmap)
{
using Getter = unsigned(__thiscall*)(const imvu::Pixmap*);
struct Getters {
Getter width;
Getter height;
};
static const Getters getters = [] {
HMODULE module = GetModuleHandleW(L"pixmap.dll");
if (module == nullptr) {
return Getters{};
}
return Getters{reinterpret_cast<Getter>(GetProcAddress(module, "?width@Pixmap@@QBEIXZ")),
reinterpret_cast<Getter>(GetProcAddress(module, "?height@Pixmap@@QBEIXZ"))};
}();
if (getters.width == nullptr || getters.height == nullptr) {
return {-1, -1};
}
return {static_cast<int>(getters.width(&pixmap)), static_cast<int>(getters.height(&pixmap))};
}
} // namespace
PassthroughEngine::PassthroughEngine(imvu::SharedPtr<imvu::PiRenderEngine> inner,
std::unique_ptr<RendererProcess> renderer)
: inner_(std::move(inner)), renderer_(std::move(renderer))
{
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, imvu::Color color)
{
if (sampled(3)) {
log::info("clear(flags {}, color {:08x})", flags, color);
}
count(3);
inner_->clear(flags, color);
}
void PassthroughEngine::setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection)
{
if (sampled(4)) {
const auto& m = projection.m;
log::info("setProjection({}x{}, [{:.3f} {:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f} {:.3f}; "
"{:.3f} {:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f} {:.3f}])",
viewport.x, viewport.y, m[0][0], m[0][1], m[0][2], m[0][3], m[1][0], m[1][1], m[1][2],
m[1][3], m[2][0], m[2][1], m[2][2], m[2][3], m[3][0], m[3][1], m[3][2], m[3][3]);
}
count(4);
inner_->setProjection(viewport, projection);
}
void PassthroughEngine::setModelSpace(const imvu::Transform& model)
{
if (sampled(5)) {
log::info("setModelSpace({})", text(model));
}
count(5);
inner_->setModelSpace(model);
}
void PassthroughEngine::setViewSpace(const imvu::Transform& view)
{
if (sampled(6)) {
log::info("setViewSpace({})", text(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)
{
if (sampled(10)) {
log::info("setBlendMode({})", static_cast<int>(mode));
}
count(10);
inner_->setBlendMode(mode);
}
void PassthroughEngine::setAlphaTestingOn(bool on)
{
count(11);
inner_->setAlphaTestingOn(on);
}
void PassthroughEngine::setAlphaTestValue(float value)
{
if (sampled(12)) {
log::info("setAlphaTestValue({:.3f})", 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)
{
if (sampled(17)) {
log::info("setLightProperties({}, enabled {}, diffuse {:08x}, vector {}, type {})", index,
light.enabled, light.diffuse, text(light.vector), static_cast<int>(light.type));
}
count(17);
inner_->setLightProperties(index, light);
}
void PassthroughEngine::setFogOn(bool on)
{
count(18);
inner_->setFogOn(on);
}
void PassthroughEngine::setFogAttributes(float start, float end, imvu::Color color)
{
if (sampled(19)) {
log::info("setFogAttributes(start {:.3f}, end {:.3f}, color {:08x})", start, end, 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)
{
if (sampled(21)) {
const auto& m = transform.m;
log::info("setTextureTransformMatrix({}, [{:.3f} {:.3f} {:.3f} {:.3f} {:.3f} {:.3f}])", stage, m[0],
m[1], m[2], m[3], m[4], m[5]);
}
count(21);
inner_->setTextureTransformMatrix(stage, transform);
}
void PassthroughEngine::setEnvironmentAmbient(imvu::Color color)
{
if (sampled(22)) {
log::info("setEnvironmentAmbient({:08x})", color);
}
count(22);
inner_->setEnvironmentAmbient(color);
}
imvu::PiRenderTexture* PassthroughEngine::newRenderTexture(const imvu::Pixmap& pixmap, std::uint32_t flags)
{
if (sampled(23)) {
const auto size = pixmap_size(pixmap);
log::info("newRenderTexture({}x{} pixmap, flags {:x})", size.x, size.y, flags);
}
count(23);
return inner_->newRenderTexture(pixmap, flags);
}
imvu::PiInternalIndexBuffer* PassthroughEngine::newIndexBuffer(std::uint32_t indexCount, const std::uint16_t* indices)
{
if (sampled(24)) {
log::info("newIndexBuffer({} indices)", indexCount);
}
count(24);
return inner_->newIndexBuffer(indexCount, indices);
}
imvu::PiInternalVertexBuffer* PassthroughEngine::newStaticVertexBuffer(const void* data, std::uint32_t stride,
std::uint32_t elementSize, std::uint32_t elementCount)
{
if (sampled(25)) {
log::info("newStaticVertexBuffer(stride {}, element size {}, {} elements)", stride, elementSize,
elementCount);
}
count(25);
return inner_->newStaticVertexBuffer(data, stride, elementSize, elementCount);
}
imvu::PiInternalVertexBuffer* PassthroughEngine::newDynamicVertexBuffer(std::uint32_t elementSize, std::uint32_t elementCount)
{
if (sampled(26)) {
log::info("newDynamicVertexBuffer(element size {}, {} elements)", elementSize, elementCount);
}
count(26);
return inner_->newDynamicVertexBuffer(elementSize, elementCount);
}
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 (renderer_) {
renderer_->frameDrawn();
}
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* texCoords,
imvu::PiInternalVertexBuffer* colors)
{
count(31);
inner_->drawTriangles(indices, positions, normals, texCoords, colors);
}
void PassthroughEngine::drawLine(imvu::Vec3f from, imvu::Vec3f to, std::uint32_t color)
{
if (sampled(32)) {
log::info("drawLine({}, {}, {:08x})", text(from), text(to), 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<imvu::Color>& colors)
{
if (sampled(34)) {
log::info("drawTrianglesFromMemory({} vertices, {} indices; positions {} stride {}, colors {} stride {})",
vertexCount, indexCount, positions.count, positions.stride, colors.count, colors.stride);
}
count(34);
inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, colors);
}
} // namespace t5de
+45
View File
@@ -0,0 +1,45 @@
#include "pixmap.hpp"
#include <windows.h>
namespace t5de {
namespace {
using Getter = unsigned(__thiscall*)(const imvu::Pixmap*);
using PixelsGetter = std::uint32_t*(__thiscall*)(const imvu::Pixmap*);
struct Methods {
Getter width = nullptr;
Getter height = nullptr;
Getter rowBytes = nullptr;
PixelsGetter pixels = nullptr;
};
const Methods& methods()
{
static const Methods methods = [] {
Methods found;
if (HMODULE module = GetModuleHandleW(L"pixmap.dll")) {
found.width = reinterpret_cast<Getter>(GetProcAddress(module, "?width@Pixmap@@QBEIXZ"));
found.height = reinterpret_cast<Getter>(GetProcAddress(module, "?height@Pixmap@@QBEIXZ"));
found.rowBytes = reinterpret_cast<Getter>(GetProcAddress(module, "?rowBytes@Pixmap@@QBEIXZ"));
found.pixels = reinterpret_cast<PixelsGetter>(GetProcAddress(module, "?pixels@Pixmap@@QBEPAIXZ"));
}
return found;
}();
return methods;
}
} // namespace
std::optional<PixmapPixels> pixmap_pixels(const imvu::Pixmap& pixmap)
{
const Methods& m = methods();
if (m.width == nullptr || m.height == nullptr || m.rowBytes == nullptr || m.pixels == nullptr) {
return std::nullopt;
}
return PixmapPixels{m.width(&pixmap), m.height(&pixmap), m.rowBytes(&pixmap), m.pixels(&pixmap)};
}
} // namespace t5de
+21
View File
@@ -0,0 +1,21 @@
#pragma once
// Reading IMVU's images (Pixmap) through pixmap.dll's exported methods, rather than depending on their layout.
#include <cstdint>
#include <optional>
#include "t5de/imvu/types.hpp"
namespace t5de {
struct PixmapPixels {
std::uint32_t width;
std::uint32_t height;
std::uint32_t rowBytes;
const std::uint32_t* pixels; // 0xAARRGGBB, rows rowBytes apart
};
// Null if pixmap.dll's methods can't be found.
std::optional<PixmapPixels> pixmap_pixels(const imvu::Pixmap& pixmap);
} // namespace t5de
+18 -5
View File
@@ -1,12 +1,15 @@
// The renderer plugin's entry points, which SceneWindow.dll's createDLLRenderer looks up by name (exported
// undecorated by plugin.def). For now the renderer is the pass-through: IMVU's Direct3D engine, wrapped.
// undecorated by plugin.def). The engine is the adapter's (engine.hpp): IMVU's Direct3D engine draws, and the
// renderer process draws over part of it.
#include <windows.h>
#include <exception>
#include <string_view>
#include "log.hpp"
#include "passthrough_engine.hpp"
#include "engine.hpp"
#include "recorder.hpp"
#include "renderer_process.hpp"
#include "t5de/imvu/render_engine.hpp"
@@ -30,6 +33,15 @@ imvu::SharedPtr<imvu::PiRenderEngine> create_imvu_engine(imvu::SharedPtr<imvu::I
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.
protocol::View view()
{
wchar_t value[16] = {};
GetEnvironmentVariableW(L"T5DE_RENDERER_VIEW", value, 16);
const std::wstring_view setting = value;
return setting == L"off" ? protocol::View::Off : setting == L"full" ? protocol::View::Full : protocol::View::Split;
}
} // namespace
extern "C" int __cdecl isRenderEngineSupported() { return 1; }
@@ -45,9 +57,10 @@ extern "C" imvu::SharedPtr<imvu::PiRenderEngine> __cdecl createRenderEngine(imvu
if (!inner) {
return {};
}
auto renderer = hwnd != nullptr ? RendererProcess::start(hwnd) : nullptr;
return imvu::SharedPtr<imvu::PiRenderEngine>(
std::make_unique<PassthroughEngine>(std::move(inner), std::move(renderer)));
const protocol::View shown = view();
auto renderer = hwnd != nullptr && shown != protocol::View::Off ? RendererProcess::start(hwnd, shown) : nullptr;
return imvu::SharedPtr<imvu::PiRenderEngine>(std::make_unique<Engine>(
std::move(inner), std::make_shared<Recorder>(std::move(renderer))));
} catch (const std::exception& e) {
// Exceptions mustn't cross into IMVU's code (a different C++ runtime).
log::info("createRenderEngine failed: {}", e.what());
+111
View File
@@ -0,0 +1,111 @@
#include "recorder.hpp"
#include <cstring>
#include "log.hpp"
#include "pixmap.hpp"
namespace t5de {
namespace {
// Between frames, recorded commands (mostly textures and buffers being loaded) are sent once they reach this size.
constexpr std::size_t flushSize = 4 * 1024 * 1024;
} // namespace
void pack(std::byte* destination, const void* source, std::uint32_t stride, std::uint32_t elementSize,
std::uint32_t count)
{
const auto* from = static_cast<const std::byte*>(source);
if (stride == 0 || stride == elementSize) {
std::memcpy(destination, from, std::size_t{elementSize} * count);
return;
}
for (std::uint32_t i = 0; i < count; ++i) {
std::memcpy(destination + std::size_t{i} * elementSize, from + std::size_t{i} * stride, elementSize);
}
}
Recorder::Recorder(std::unique_ptr<RendererProcess> process) : process_(std::move(process)) {}
void Recorder::flush(bool endsFrame)
{
if (!active()) {
writer_.clear();
return;
}
process_->send(writer_.bytes(), endsFrame);
writer_.clear();
}
void Recorder::flushIfLarge()
{
if (writer_.size() >= flushSize) {
flush(false);
}
}
void Recorder::addPixels(protocol::Op op, std::uint32_t id, const imvu::Pixmap& pixmap, std::uint32_t flags)
{
const auto pixels = pixmap_pixels(pixmap);
if (!pixels || pixels->pixels == nullptr) {
return;
}
const std::size_t rowSize = std::size_t{pixels->width} * 4;
std::byte* payload = nullptr;
if (op == protocol::Op::CreateTexture) {
payload = add(op, protocol::CreateTexture{id, pixels->width, pixels->height, flags}, rowSize * pixels->height);
} else {
payload = add(op, protocol::UpdateTexture{id, pixels->width, pixels->height}, rowSize * pixels->height);
}
if (payload == nullptr) {
return;
}
const auto* rows = reinterpret_cast<const std::byte*>(pixels->pixels);
for (std::uint32_t y = 0; y < pixels->height; ++y) {
std::memcpy(payload + y * rowSize, rows + std::size_t{y} * pixels->rowBytes, rowSize);
}
flushIfLarge();
}
std::uint32_t Recorder::createTexture(const imvu::Pixmap& pixmap, std::uint32_t flags)
{
const std::uint32_t id = nextId_++;
addPixels(protocol::Op::CreateTexture, id, pixmap, flags);
return id;
}
void Recorder::updateTexture(std::uint32_t id, const imvu::Pixmap& pixmap)
{
addPixels(protocol::Op::UpdateTexture, id, pixmap, 0);
}
void Recorder::destroyTexture(std::uint32_t id) { add(protocol::Op::DestroyTexture, protocol::DestroyTexture{id}); }
std::uint32_t Recorder::createBuffer(protocol::BufferKind kind, const void* data, std::uint32_t stride,
std::uint32_t elementSize, std::uint32_t count)
{
const std::uint32_t id = nextId_++;
const std::size_t size = data != nullptr ? std::size_t{elementSize} * count : 0;
if (std::byte* payload = add(protocol::Op::CreateBuffer, protocol::CreateBuffer{id, kind, elementSize, count}, size);
payload != nullptr && size != 0) {
pack(payload, data, stride, elementSize, count);
}
flushIfLarge();
return id;
}
void Recorder::updateBuffer(std::uint32_t id, const void* data, std::uint32_t stride, std::uint32_t elementSize,
std::uint32_t count)
{
const std::size_t size = std::size_t{elementSize} * count;
if (std::byte* payload = add(protocol::Op::UpdateBuffer, protocol::UpdateBuffer{id, elementSize, count}, size);
payload != nullptr && data != nullptr) {
pack(payload, data, stride, elementSize, count);
}
}
void Recorder::destroyBuffer(std::uint32_t id) { add(protocol::Op::DestroyBuffer, protocol::DestroyBuffer{id}); }
} // namespace t5de
+59
View File
@@ -0,0 +1,59 @@
#pragma once
// Records what the client draws as commands (include/t5de/protocol/commands.hpp) and sends them to the renderer
// process. Shared by an engine and the resources it creates (which can outlive it); once the renderer process is
// gone, it records nothing.
#include <cstdint>
#include <memory>
#include "renderer_process.hpp"
#include "t5de/imvu/types.hpp"
#include "t5de/protocol/commands.hpp"
namespace t5de {
class Recorder {
public:
explicit Recorder(std::unique_ptr<RendererProcess> process);
[[nodiscard]] bool active() const { return process_ && process_->alive(); }
// Records a command (if active); returns its payload to fill, or null.
template <class Body>
std::byte* add(protocol::Op op, const Body& body, std::size_t payloadSize = 0)
{
if (!active()) {
return nullptr;
}
std::byte* payload = writer_.add(op, body, payloadSize);
return payload;
}
// Sends what's recorded: at the end of each frame, and between frames when a lot is waiting (resources).
void flush(bool endsFrame);
void flushIfLarge();
std::uint32_t createTexture(const imvu::Pixmap& pixmap, std::uint32_t flags);
void updateTexture(std::uint32_t id, const imvu::Pixmap& pixmap);
void destroyTexture(std::uint32_t id);
// Copies count elements of elementSize bytes, stride bytes apart (0: packed), from data (null: none yet).
std::uint32_t createBuffer(protocol::BufferKind kind, const void* data, std::uint32_t stride,
std::uint32_t elementSize, std::uint32_t count);
void updateBuffer(std::uint32_t id, const void* data, std::uint32_t stride, std::uint32_t elementSize,
std::uint32_t count);
void destroyBuffer(std::uint32_t id);
private:
void addPixels(protocol::Op op, std::uint32_t id, const imvu::Pixmap& pixmap, std::uint32_t flags);
std::unique_ptr<RendererProcess> process_;
protocol::Writer writer_;
std::uint32_t nextId_ = 1;
};
// Copies count elements of elementSize bytes, stride bytes apart (0: packed), into destination, packed.
void pack(std::byte* destination, const void* source, std::uint32_t stride, std::uint32_t elementSize,
std::uint32_t count);
} // namespace t5de
+48 -16
View File
@@ -1,6 +1,8 @@
#include "renderer_process.hpp"
#include <algorithm>
#include <atomic>
#include <cstring>
#include <filesystem>
#include <format>
#include <string>
@@ -11,6 +13,9 @@ namespace t5de {
namespace {
// How long to wait for the renderer process to take a chunk before giving up on it.
constexpr DWORD consumeTimeout = 5000;
// The folder this DLL is in: the client's program folder.
std::filesystem::path module_folder()
{
@@ -22,31 +27,39 @@ std::filesystem::path module_folder()
return std::filesystem::path(path).parent_path();
}
std::string narrow(const std::wstring& text) { return std::filesystem::path(text).string(); }
} // namespace
std::unique_ptr<RendererProcess> RendererProcess::start(HWND window)
std::unique_ptr<RendererProcess> RendererProcess::start(HWND window, protocol::View view)
{
static std::atomic<int> engines = 0;
const std::wstring name = std::format(L"Local\\t5de-renderer-{}-{}", GetCurrentProcessId(), engines++);
constexpr DWORD size = sizeof(protocol::Channel) + protocol::channelCapacity;
std::unique_ptr<RendererProcess> renderer(new RendererProcess());
renderer->mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0,
sizeof(protocol::Channel), name.c_str());
renderer->mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, size, name.c_str());
if (renderer->mapping_ == nullptr) {
log::info("renderer process: can't create the channel: error {}", GetLastError());
return nullptr;
}
renderer->channel_ = static_cast<protocol::Channel*>(
MapViewOfFile(renderer->mapping_, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(protocol::Channel)));
renderer->frameEvent_ = CreateEventW(nullptr, FALSE, FALSE, (name + protocol::frameEventSuffix).c_str());
if (renderer->channel_ == nullptr || renderer->frameEvent_ == nullptr) {
renderer->channel_ =
static_cast<protocol::Channel*>(MapViewOfFile(renderer->mapping_, FILE_MAP_ALL_ACCESS, 0, 0, size));
renderer->ready_ = CreateEventW(nullptr, FALSE, FALSE, (name + protocol::readyEventSuffix).c_str());
renderer->consumed_ = CreateEventW(nullptr, FALSE, TRUE, (name + protocol::consumedEventSuffix).c_str());
if (renderer->channel_ == nullptr || renderer->ready_ == nullptr || renderer->consumed_ == nullptr) {
log::info("renderer process: can't set up the channel: error {}", GetLastError());
return nullptr;
}
renderer->data_ = reinterpret_cast<std::byte*>(renderer->channel_ + 1);
*renderer->channel_ = {.version = protocol::version,
.closing = 0,
.window = reinterpret_cast<std::uintptr_t>(window),
.frames = 0};
.frames = 0,
.view = view,
.capacity = protocol::channelCapacity,
.chunkSize = 0,
.chunkFlags = 0};
// 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);
@@ -62,8 +75,7 @@ std::unique_ptr<RendererProcess> RendererProcess::start(HWND window)
}
CloseHandle(process.hThread);
renderer->process_ = process.hProcess;
log::info("renderer process: started {} (pid {}) on channel {}", exe.string(), process.dwProcessId,
std::filesystem::path(name).string());
log::info("renderer process: started {} (pid {}) on channel {}", exe.string(), process.dwProcessId, narrow(name));
return renderer;
}
@@ -71,22 +83,42 @@ RendererProcess::~RendererProcess()
{
if (channel_ != nullptr) {
channel_->closing = 1;
if (frameEvent_ != nullptr) {
SetEvent(frameEvent_);
if (ready_ != nullptr) {
SetEvent(ready_);
}
UnmapViewOfFile(channel_);
}
for (HANDLE handle : {frameEvent_, mapping_, process_}) {
for (HANDLE handle : {ready_, consumed_, mapping_, process_}) {
if (handle != nullptr) {
CloseHandle(handle);
}
}
}
void RendererProcess::frameDrawn()
bool RendererProcess::send(std::span<const std::byte> commands, bool endsFrame)
{
++channel_->frames;
SetEvent(frameEvent_);
if (!alive_) {
return false;
}
do {
const HANDLE waits[] = {consumed_, process_};
const DWORD result = WaitForMultipleObjects(2, waits, FALSE, consumeTimeout);
if (result != WAIT_OBJECT_0) {
log::info("renderer process: {}; giving up on it", result == WAIT_OBJECT_0 + 1 ? "exited" : "not responding");
alive_ = false;
return false;
}
const std::size_t size = std::min<std::size_t>(commands.size(), channel_->capacity);
std::memcpy(data_, commands.data(), size);
commands = commands.subspan(size);
channel_->chunkSize = static_cast<std::uint32_t>(size);
channel_->chunkFlags = endsFrame && commands.empty() ? protocol::chunkEndsFrame : 0;
if (endsFrame && commands.empty()) {
++channel_->frames;
}
SetEvent(ready_);
} while (!commands.empty());
return true;
}
} // namespace t5de
+15 -8
View File
@@ -1,11 +1,12 @@
#pragma once
// The renderer process (t5de-renderer.exe, 64-bit) an engine draws through: started with the engine, told about
// each frame through a shared memory channel (include/t5de/protocol/channel.hpp), and told to exit with it.
// The renderer process (t5de-renderer.exe, 64-bit) an engine draws through: started with the engine, sent the
// command stream through a shared memory channel (include/t5de/protocol/channel.hpp), and told to exit with it.
#include <windows.h>
#include <cstdint>
#include <cstddef>
#include <memory>
#include <span>
#include "t5de/protocol/channel.hpp"
@@ -13,23 +14,29 @@ namespace t5de {
class RendererProcess {
public:
// Starts t5de-renderer.exe from this DLL's folder, drawing into window; null if it can't.
static std::unique_ptr<RendererProcess> start(HWND window);
// Starts t5de-renderer.exe from this DLL's folder, drawing into part of window; null if it can't.
static std::unique_ptr<RendererProcess> start(HWND window, protocol::View view);
RendererProcess(const RendererProcess&) = delete;
RendererProcess& operator=(const RendererProcess&) = delete;
~RendererProcess();
// The client drew a frame.
void frameDrawn();
// Sends commands, in as many chunks as they take; endsFrame marks the end of a frame. False if the renderer
// process is gone or stopped taking them (it's then given up on).
bool send(std::span<const std::byte> commands, bool endsFrame);
[[nodiscard]] bool alive() const { return alive_; }
private:
RendererProcess() = default;
HANDLE mapping_ = nullptr;
HANDLE frameEvent_ = nullptr;
HANDLE ready_ = nullptr;
HANDLE consumed_ = nullptr;
HANDLE process_ = nullptr;
protocol::Channel* channel_ = nullptr;
std::byte* data_ = nullptr;
bool alive_ = true;
};
} // namespace t5de
+62
View File
@@ -0,0 +1,62 @@
#include "resources.hpp"
namespace t5de {
Texture::Texture(imvu::PiRenderTexture* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
: inner_(inner), recorder_(std::move(recorder)), id_(id)
{
}
Texture::~Texture()
{
delete inner_; // through its virtual destructor: IMVU's code frees it
recorder_->destroyTexture(id_);
}
imvu::Vec2i Texture::size() const { return inner_->size(); }
void Texture::updateRegions(const imvu::Pixmap& pixmap, const imvu::CompoundRegion& regions)
{
inner_->updateRegions(pixmap, regions);
recorder_->updateTexture(id_, pixmap); // all of it: the regions' layout isn't known
}
void Texture::update(const imvu::Pixmap& pixmap)
{
inner_->update(pixmap);
recorder_->updateTexture(id_, pixmap);
}
imvu::Pixmap* Texture::read(imvu::Pixmap* result) const { return inner_->read(result); }
IndexBuffer::IndexBuffer(imvu::PiInternalIndexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
: inner_(inner), recorder_(std::move(recorder)), id_(id)
{
}
IndexBuffer::~IndexBuffer()
{
delete inner_;
recorder_->destroyBuffer(id_);
}
VertexBuffer::VertexBuffer(imvu::PiInternalVertexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
: inner_(inner), recorder_(std::move(recorder)), id_(id)
{
}
VertexBuffer::~VertexBuffer()
{
delete inner_;
recorder_->destroyBuffer(id_);
}
void VertexBuffer::update(const void* data, std::uint32_t stride, std::uint32_t elementSize, std::uint32_t elementCount)
{
inner_->update(data, stride, elementSize, elementCount);
recorder_->updateBuffer(id_, data, stride, elementSize, elementCount);
}
void VertexBuffer::unknown2() { inner_->unknown2(); }
} // namespace t5de
+91
View File
@@ -0,0 +1,91 @@
#pragma once
// The textures and buffers the adapter's engine hands to the client: each wraps the client renderer's own (which the
// client renderer still draws with) and mirrors it, by id, to the renderer process. The client deletes them through
// their virtual destructors.
#include <cstdint>
#include <memory>
#include "recorder.hpp"
#include "t5de/imvu/resources.hpp"
namespace t5de {
class Texture final : public imvu::PiRenderTexture {
public:
Texture(imvu::PiRenderTexture* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id);
~Texture() override;
imvu::Vec2i size() const override;
void updateRegions(const imvu::Pixmap& pixmap, const imvu::CompoundRegion& regions) override;
void update(const imvu::Pixmap& pixmap) override;
imvu::Pixmap* read(imvu::Pixmap* result) const override;
[[nodiscard]] imvu::PiRenderTexture* inner() const { return inner_; }
[[nodiscard]] std::uint32_t id() const { return id_; }
private:
imvu::PiRenderTexture* inner_;
std::shared_ptr<Recorder> recorder_;
std::uint32_t id_;
};
class IndexBuffer final : public imvu::PiInternalIndexBuffer {
public:
IndexBuffer(imvu::PiInternalIndexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id);
~IndexBuffer() override;
[[nodiscard]] imvu::PiInternalIndexBuffer* inner() const { return inner_; }
[[nodiscard]] std::uint32_t id() const { return id_; }
private:
imvu::PiInternalIndexBuffer* inner_;
std::shared_ptr<Recorder> recorder_;
std::uint32_t id_;
};
class VertexBuffer final : public imvu::PiInternalVertexBuffer {
public:
VertexBuffer(imvu::PiInternalVertexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id);
~VertexBuffer() override;
void update(const void* data, std::uint32_t stride, std::uint32_t elementSize,
std::uint32_t elementCount) override;
void unknown2() override;
[[nodiscard]] imvu::PiInternalVertexBuffer* inner() const { return inner_; }
[[nodiscard]] std::uint32_t id() const { return id_; }
private:
imvu::PiInternalVertexBuffer* inner_;
std::shared_ptr<Recorder> recorder_;
std::uint32_t id_;
};
// The client renderer's object and our id for something the engine made; null and 0 for null.
inline imvu::PiRenderTexture* inner(imvu::PiRenderTexture* texture)
{
return texture != nullptr ? static_cast<Texture*>(texture)->inner() : nullptr;
}
inline std::uint32_t id(imvu::PiRenderTexture* texture)
{
return texture != nullptr ? static_cast<Texture*>(texture)->id() : 0;
}
inline imvu::PiInternalIndexBuffer* inner(imvu::PiInternalIndexBuffer* buffer)
{
return buffer != nullptr ? static_cast<IndexBuffer*>(buffer)->inner() : nullptr;
}
inline std::uint32_t id(imvu::PiInternalIndexBuffer* buffer)
{
return buffer != nullptr ? static_cast<IndexBuffer*>(buffer)->id() : 0;
}
inline imvu::PiInternalVertexBuffer* inner(imvu::PiInternalVertexBuffer* buffer)
{
return buffer != nullptr ? static_cast<VertexBuffer*>(buffer)->inner() : nullptr;
}
inline std::uint32_t id(imvu::PiInternalVertexBuffer* buffer)
{
return buffer != nullptr ? static_cast<VertexBuffer*>(buffer)->id() : 0;
}
} // namespace t5de