diff --git a/adapter/CMakeLists.txt b/adapter/CMakeLists.txt index 5a48310..58fa99d 100644 --- a/adapter/CMakeLists.txt +++ b/adapter/CMakeLists.txt @@ -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) diff --git a/adapter/engine.cpp b/adapter/engine.cpp new file mode 100644 index 0000000..8aeb4a2 --- /dev/null +++ b/adapter/engine.cpp @@ -0,0 +1,296 @@ +#include "engine.hpp" + +#include +#include +#include + +#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 inner, std::shared_ptr 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(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::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 Engine::newPixmapRenderTarget(imvu::Vec2i size) +{ + return inner_->newPixmapRenderTarget(size); +} + +imvu::SharedPtr 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& positions, + const imvu::VertexAttributeArray& normals, + const imvu::VertexAttributeArray& texCoords, + const imvu::VertexAttributeArray& 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 diff --git a/adapter/passthrough_engine.hpp b/adapter/engine.hpp similarity index 79% rename from adapter/passthrough_engine.hpp rename to adapter/engine.hpp index e570832..fa148b7 100644 --- a/adapter/passthrough_engine.hpp +++ b/adapter/engine.hpp @@ -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 #include #include -#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 inner, std::unique_ptr renderer); - ~PassthroughEngine() override; + // recorder may be inactive (no renderer process). + Engine(imvu::SharedPtr inner, std::shared_ptr 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& positions, @@ -61,15 +61,11 @@ public: const imvu::VertexAttributeArray& texCoords, const imvu::VertexAttributeArray& 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 inner_; - std::unique_ptr renderer_; - std::array calls_{}; + std::shared_ptr recorder_; std::uint64_t frames_ = 0; }; diff --git a/adapter/passthrough_engine.cpp b/adapter/passthrough_engine.cpp deleted file mode 100644 index 14474a8..0000000 --- a/adapter/passthrough_engine.cpp +++ /dev/null @@ -1,395 +0,0 @@ -#include "passthrough_engine.hpp" - -#include - -#include -#include - -#include "log.hpp" - -namespace t5de { - -namespace { - -constexpr std::array 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, 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(GetProcAddress(module, "?width@Pixmap@@QBEIXZ")), - reinterpret_cast(GetProcAddress(module, "?height@Pixmap@@QBEIXZ"))}; - }(); - if (getters.width == nullptr || getters.height == nullptr) { - return {-1, -1}; - } - return {static_cast(getters.width(&pixmap)), static_cast(getters.height(&pixmap))}; -} - -} // namespace - -PassthroughEngine::PassthroughEngine(imvu::SharedPtr inner, - std::unique_ptr renderer) - : inner_(std::move(inner)), renderer_(std::move(renderer)) -{ - log::info("passthrough: wrapping the engine at {}", static_cast(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(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(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 PassthroughEngine::newPixmapRenderTarget(imvu::Vec2i size) -{ - log::info("newPixmapRenderTarget({}, {})", size.x, size.y); - count(27); - return inner_->newPixmapRenderTarget(size); -} - -imvu::SharedPtr 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& positions, - const imvu::VertexAttributeArray& normals, - const imvu::VertexAttributeArray& texCoords, - const imvu::VertexAttributeArray& 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 diff --git a/adapter/pixmap.cpp b/adapter/pixmap.cpp new file mode 100644 index 0000000..cb2ee82 --- /dev/null +++ b/adapter/pixmap.cpp @@ -0,0 +1,45 @@ +#include "pixmap.hpp" + +#include + +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(GetProcAddress(module, "?width@Pixmap@@QBEIXZ")); + found.height = reinterpret_cast(GetProcAddress(module, "?height@Pixmap@@QBEIXZ")); + found.rowBytes = reinterpret_cast(GetProcAddress(module, "?rowBytes@Pixmap@@QBEIXZ")); + found.pixels = reinterpret_cast(GetProcAddress(module, "?pixels@Pixmap@@QBEPAIXZ")); + } + return found; + }(); + return methods; +} + +} // namespace + +std::optional 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 diff --git a/adapter/pixmap.hpp b/adapter/pixmap.hpp new file mode 100644 index 0000000..8ac18fe --- /dev/null +++ b/adapter/pixmap.hpp @@ -0,0 +1,21 @@ +#pragma once +// Reading IMVU's images (Pixmap) through pixmap.dll's exported methods, rather than depending on their layout. + +#include +#include + +#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 pixmap_pixels(const imvu::Pixmap& pixmap); + +} // namespace t5de diff --git a/adapter/plugin.cpp b/adapter/plugin.cpp index 96dd0cc..ef7c83e 100644 --- a/adapter/plugin.cpp +++ b/adapter/plugin.cpp @@ -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 #include +#include #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 create_imvu_engine(imvu::SharedPtr __cdecl createRenderEngine(imvu if (!inner) { return {}; } - auto renderer = hwnd != nullptr ? RendererProcess::start(hwnd) : nullptr; - return imvu::SharedPtr( - std::make_unique(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(std::make_unique( + std::move(inner), std::make_shared(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()); diff --git a/adapter/recorder.cpp b/adapter/recorder.cpp new file mode 100644 index 0000000..3c72ef7 --- /dev/null +++ b/adapter/recorder.cpp @@ -0,0 +1,111 @@ +#include "recorder.hpp" + +#include + +#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(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 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(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 diff --git a/adapter/recorder.hpp b/adapter/recorder.hpp new file mode 100644 index 0000000..28bd46a --- /dev/null +++ b/adapter/recorder.hpp @@ -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 +#include + +#include "renderer_process.hpp" +#include "t5de/imvu/types.hpp" +#include "t5de/protocol/commands.hpp" + +namespace t5de { + +class Recorder { +public: + explicit Recorder(std::unique_ptr process); + + [[nodiscard]] bool active() const { return process_ && process_->alive(); } + + // Records a command (if active); returns its payload to fill, or null. + template + 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 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 diff --git a/adapter/renderer_process.cpp b/adapter/renderer_process.cpp index 64cee38..fa866f4 100644 --- a/adapter/renderer_process.cpp +++ b/adapter/renderer_process.cpp @@ -1,6 +1,8 @@ #include "renderer_process.hpp" +#include #include +#include #include #include #include @@ -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::start(HWND window) +std::unique_ptr RendererProcess::start(HWND window, protocol::View view) { static std::atomic 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 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( - 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(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(renderer->channel_ + 1); *renderer->channel_ = {.version = protocol::version, .closing = 0, .window = reinterpret_cast(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::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 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(commands.size(), channel_->capacity); + std::memcpy(data_, commands.data(), size); + commands = commands.subspan(size); + channel_->chunkSize = static_cast(size); + channel_->chunkFlags = endsFrame && commands.empty() ? protocol::chunkEndsFrame : 0; + if (endsFrame && commands.empty()) { + ++channel_->frames; + } + SetEvent(ready_); + } while (!commands.empty()); + return true; } } // namespace t5de diff --git a/adapter/renderer_process.hpp b/adapter/renderer_process.hpp index 470cd0d..9049021 100644 --- a/adapter/renderer_process.hpp +++ b/adapter/renderer_process.hpp @@ -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 -#include +#include #include +#include #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 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 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 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 diff --git a/adapter/resources.cpp b/adapter/resources.cpp new file mode 100644 index 0000000..475b99f --- /dev/null +++ b/adapter/resources.cpp @@ -0,0 +1,62 @@ +#include "resources.hpp" + +namespace t5de { + +Texture::Texture(imvu::PiRenderTexture* inner, std::shared_ptr 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, 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, 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 diff --git a/adapter/resources.hpp b/adapter/resources.hpp new file mode 100644 index 0000000..a0c5e34 --- /dev/null +++ b/adapter/resources.hpp @@ -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 +#include + +#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, 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_; + std::uint32_t id_; +}; + +class IndexBuffer final : public imvu::PiInternalIndexBuffer { +public: + IndexBuffer(imvu::PiInternalIndexBuffer* inner, std::shared_ptr 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_; + std::uint32_t id_; +}; + +class VertexBuffer final : public imvu::PiInternalVertexBuffer { +public: + VertexBuffer(imvu::PiInternalVertexBuffer* inner, std::shared_ptr 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_; + 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)->inner() : nullptr; +} +inline std::uint32_t id(imvu::PiRenderTexture* texture) +{ + return texture != nullptr ? static_cast(texture)->id() : 0; +} +inline imvu::PiInternalIndexBuffer* inner(imvu::PiInternalIndexBuffer* buffer) +{ + return buffer != nullptr ? static_cast(buffer)->inner() : nullptr; +} +inline std::uint32_t id(imvu::PiInternalIndexBuffer* buffer) +{ + return buffer != nullptr ? static_cast(buffer)->id() : 0; +} +inline imvu::PiInternalVertexBuffer* inner(imvu::PiInternalVertexBuffer* buffer) +{ + return buffer != nullptr ? static_cast(buffer)->inner() : nullptr; +} +inline std::uint32_t id(imvu::PiInternalVertexBuffer* buffer) +{ + return buffer != nullptr ? static_cast(buffer)->id() : 0; +} + +} // namespace t5de diff --git a/include/t5de/protocol/channel.hpp b/include/t5de/protocol/channel.hpp index 20155cb..8de6bd7 100644 --- a/include/t5de/protocol/channel.hpp +++ b/include/t5de/protocol/channel.hpp @@ -7,19 +7,36 @@ namespace t5de::protocol { -inline constexpr std::uint32_t version = 1; +inline constexpr std::uint32_t version = 2; -// A shared memory section the adapter creates for each engine. Its name is passed to the renderer process as -// `--channel `; the event "-frame" (auto-reset) is set after each frame the client draws. +// 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 + Full, +}; + +// A shared memory section the adapter creates for each engine, named by the renderer process's `--channel ` +// argument: this header, then `capacity` bytes of data. The adapter sends the command stream (commands.hpp) through +// it in chunks: it waits for the event "-consumed", writes a chunk and sets "-ready"; the renderer +// process copies the chunk out and sets "-consumed" (both auto-reset; consumed starts set). struct Channel { std::uint32_t version; std::uint32_t closing; // nonzero: the engine is gone, the renderer process should exit std::uint64_t window; // IMVU's 3D window (an HWND; window handles are valid in every process) std::uint64_t frames; // frames the client has drawn + View view; + std::uint32_t capacity; + std::uint32_t chunkSize; + std::uint32_t chunkFlags; // chunkEndsFrame }; -static_assert(sizeof(Channel) == 24, "same layout in 32-bit and 64-bit builds"); +static_assert(sizeof(Channel) == 40, "same layout in 32-bit and 64-bit builds"); -inline constexpr wchar_t frameEventSuffix[] = L"-frame"; +inline constexpr std::uint32_t chunkEndsFrame = 1; +inline constexpr std::uint32_t channelCapacity = 16 * 1024 * 1024; + +inline constexpr wchar_t readyEventSuffix[] = L"-ready"; +inline constexpr wchar_t consumedEventSuffix[] = L"-consumed"; } // namespace t5de::protocol diff --git a/include/t5de/protocol/commands.hpp b/include/t5de/protocol/commands.hpp new file mode 100644 index 0000000..7a8c03b --- /dev/null +++ b/include/t5de/protocol/commands.hpp @@ -0,0 +1,259 @@ +#pragma once +// The command stream: what the adapter records from IMVU's renderer interface, and the renderer process carries out. +// Each command is a Header, its fixed-size body, then for some a variable-size payload (pixels, vertex data), +// padded to 4 bytes. Built for 32-bit and 64-bit: fixed-size fields only. + +#include +#include +#include +#include +#include +#include + +namespace t5de::protocol { + +enum class Op : std::uint16_t { + BeginFrame = 1, + EndFrame, + Clear, + SetProjection, + SetModel, + SetView, + SetTextures, + SetState, + SetLight, + SetFog, + SetTextureTransform, + CreateTexture, + UpdateTexture, + DestroyTexture, + CreateBuffer, + UpdateBuffer, + DestroyBuffer, + DrawTriangles, + DrawArrays, +}; + +struct Header { + Op op; + std::uint16_t reserved; + std::uint32_t size; // of the body and payload, padded to 4 bytes +}; + +// Colors are 0xAARRGGBB. Resource ids are the adapter's, per engine; 0 means none. + +struct BeginFrame { + std::uint32_t offscreen; // drawn into a render target, not the window: nothing to show +}; + +struct EndFrame { + std::uint32_t unused; +}; + +struct Clear { + std::uint32_t flags; // 1: color, 2: depth (to 1) + std::uint32_t color; +}; + +// The viewport's size, and a Direct3D projection matrix (row-major, row vectors). +struct SetProjection { + std::int32_t width; + std::int32_t height; + float m[16]; +}; + +// An affine transform (SetModel, SetView): a 3x3 matrix, row-major, then the translation. +struct SetTransform { + float m[12]; +}; + +struct SetTextures { + std::uint32_t texture0; + std::uint32_t texture1; +}; + +enum class State : std::uint32_t { + DepthTest, + DepthWrite, + Blending, + BlendMode, // 1: alpha, 2: additive, 3: premultiplied, 4: additive alpha + AlphaTest, + AlphaReference, // 0-255: pixels pass if their alpha is greater + Culling, + NormalInverting, // flips which winding faces front + Lighting, + Fog, + Ambient, // a color +}; + +struct SetState { + State state; + std::uint32_t value; +}; + +struct SetLight { + std::uint32_t index; + std::uint32_t enabled; + std::uint32_t color; + std::uint32_t directional; // 0: a point light at vector; 1: light coming from direction vector + float vector[3]; +}; + +struct SetFog { + float start; + float end; + std::uint32_t color; +}; + +// Texture coordinates for a stage: u' = u * m[0] + v * m[1] + m[4], v' = u * m[2] + v * m[3] + m[5]. +struct SetTextureTransform { + std::uint32_t stage; + std::uint32_t identity; + float m[6]; +}; + +// Payload: width * height pixels (0xAARRGGBB), rows packed. +struct CreateTexture { + std::uint32_t id; + std::uint32_t width; + std::uint32_t height; + std::uint32_t flags; // 2: wrap u, 4: wrap v (else clamp) +}; + +struct UpdateTexture { + std::uint32_t id; + std::uint32_t width; + std::uint32_t height; +}; + +struct DestroyTexture { + std::uint32_t id; +}; + +enum class BufferKind : std::uint32_t { + Index, // 16-bit indices + Vertex, // positions and normals (3 floats), texture coordinates (2 floats) or colors (0xAARRGGBB) +}; + +// Payload: count elements of elementSize bytes, packed; empty for a buffer filled later (UpdateBuffer). +struct CreateBuffer { + std::uint32_t id; + BufferKind kind; + std::uint32_t elementSize; + std::uint32_t count; +}; + +struct UpdateBuffer { + std::uint32_t id; + std::uint32_t elementSize; + std::uint32_t count; +}; + +struct DestroyBuffer { + std::uint32_t id; +}; + +// Triangles from buffers: indices and positions are required, the others optional. +struct DrawTriangles { + std::uint32_t indices; + std::uint32_t positions; + std::uint32_t normals; + std::uint32_t texCoords; + std::uint32_t colors; +}; + +// Triangles from data in the payload, packed: indexCount 16-bit indices (padded to 4 bytes), then vertexCount +// positions, and normals, texture coordinates and colors if present. +struct DrawArrays { + std::uint32_t vertexCount; + std::uint32_t indexCount; + std::uint32_t hasNormals; + std::uint32_t hasTexCoords; + std::uint32_t hasColors; +}; + +static_assert(sizeof(Header) == 8 && sizeof(SetProjection) == 72 && sizeof(SetLight) == 28 + && sizeof(DrawArrays) == 20); + +inline constexpr std::size_t padded(std::size_t size) { return (size + 3) & ~std::size_t{3}; } + +// Appends commands to a byte buffer. +class Writer { +public: + // Appends a command and returns its payload, payloadSize bytes for the caller to fill. + template + std::byte* add(Op op, const Body& body, std::size_t payloadSize = 0) + { + static_assert(sizeof(Body) % 4 == 0); + const std::size_t size = sizeof(Body) + padded(payloadSize); + const std::size_t start = bytes_.size(); + bytes_.resize(start + sizeof(Header) + size); + const Header header{op, 0, static_cast(size)}; + std::memcpy(bytes_.data() + start, &header, sizeof(Header)); + std::memcpy(bytes_.data() + start + sizeof(Header), &body, sizeof(Body)); + return bytes_.data() + start + sizeof(Header) + sizeof(Body); + } + + [[nodiscard]] std::span bytes() const { return bytes_; } + [[nodiscard]] std::size_t size() const { return bytes_.size(); } + void clear() { bytes_.clear(); } + +private: + std::vector bytes_; +}; + +// One command, as read back. +class Command { +public: + Command(Op op, std::span data) : op_(op), data_(data) {} + + [[nodiscard]] Op op() const { return op_; } + + // The body, if the command is long enough to hold one. + template + [[nodiscard]] std::optional body() const + { + if (data_.size() < sizeof(Body)) { + return std::nullopt; + } + Body body; + std::memcpy(&body, data_.data(), sizeof(Body)); + return body; + } + + template + [[nodiscard]] std::span payload() const + { + return data_.size() < sizeof(Body) ? std::span{} : data_.subspan(sizeof(Body)); + } + +private: + Op op_; + std::span data_; +}; + +// Reads commands back from a byte buffer; stops at the end, or at a truncated command. +class Reader { +public: + explicit Reader(std::span bytes) : bytes_(bytes) {} + + std::optional next() + { + if (bytes_.size() < sizeof(Header)) { + return std::nullopt; + } + Header header; + std::memcpy(&header, bytes_.data(), sizeof(Header)); + if (bytes_.size() - sizeof(Header) < header.size) { + return std::nullopt; + } + Command command(header.op, bytes_.subspan(sizeof(Header), header.size)); + bytes_ = bytes_.subspan(sizeof(Header) + header.size); + return command; + } + +private: + std::span bytes_; +}; + +} // namespace t5de::protocol diff --git a/renderer/CMakeLists.txt b/renderer/CMakeLists.txt index dd08ff5..84935b4 100644 --- a/renderer/CMakeLists.txt +++ b/renderer/CMakeLists.txt @@ -20,8 +20,21 @@ FetchContent_Declare(DiligentCore SYSTEM) FetchContent_MakeAvailable(DiligentCore) -add_executable(t5de_renderer WIN32 main.cpp) +# Shader sources, embedded in the program (Diligent compiles them at run time): each shaders/.hlsl becomes +# _hlsl.hpp, defining Hlsl. +function(t5de_embed_shader file name) + set(T5DE_SHADER_FILE ${file}) + set(T5DE_SHADER_NAME ${name}) + file(READ ${CMAKE_CURRENT_SOURCE_DIR}/shaders/${file} T5DE_SHADER_SOURCE) + get_filename_component(stem ${file} NAME_WE) + configure_file(shaders/embed.hpp.in ${CMAKE_CURRENT_BINARY_DIR}/generated/${stem}_hlsl.hpp @ONLY) + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS shaders/${file}) +endfunction() +t5de_embed_shader(fixed_function.hlsl fixedFunctionHlsl) + +add_executable(t5de_renderer WIN32 main.cpp scene.cpp) set_target_properties(t5de_renderer PROPERTIES OUTPUT_NAME t5de-renderer) +target_include_directories(t5de_renderer PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/generated) target_link_libraries(t5de_renderer PRIVATE t5de::protocol t5de::options Diligent-GraphicsEngineD3D11-static Diligent-Common) install(TARGETS t5de_renderer RUNTIME DESTINATION .) diff --git a/renderer/main.cpp b/renderer/main.cpp index 5faec89..e6f49e7 100644 --- a/renderer/main.cpp +++ b/renderer/main.cpp @@ -1,31 +1,63 @@ // t5de-renderer.exe: the renderer process. Started by the adapter (t5deRenderEngine.dll) for each of the client's -// 3D views; draws into a child window it places in the client's window, frame by frame as the client draws. -// -// For now a spike proving the architecture: a small box in the 3D view's corner, its color changing with each frame -// the client draws, drawn with Diligent (Direct3D 11) from this 64-bit process. +// 3D views: receives the client's drawing as commands through a shared memory channel, and draws it with Diligent +// into a child window it places in the client's window (over the right half, for now, to compare with the client's +// own renderer on the left). #include +#include #include -#include #include +#include +#include +#include +#include #include #include +#include #include "EngineFactoryD3D11.h" #include "RefCntAutoPtr.hpp" +#include "scene.hpp" #include "t5de/protocol/channel.hpp" namespace { using namespace Diligent; +using t5de::protocol::Channel; +using t5de::protocol::View; -// The box: where in the client's 3D view, and how big. -constexpr int boxX = 16; -constexpr int boxY = 16; -constexpr int boxWidth = 240; -constexpr int boxHeight = 80; +// The renderer processes' log: %TEMP%\t5de-renderer-process.log, shared by all of them (lines start with the pid). +void write_log(std::string_view line) +{ + static std::FILE* file = [] { + wchar_t temp[MAX_PATH + 1]; + const DWORD length = GetTempPathW(MAX_PATH + 1, temp); + return length == 0 || length > MAX_PATH + ? nullptr + : _wfsopen((std::filesystem::path(temp) / L"t5de-renderer-process.log").c_str(), L"a", _SH_DENYNO); + }(); + if (file != nullptr) { + std::fprintf(file, "%lu: %.*s\n", GetCurrentProcessId(), static_cast(line.size()), line.data()); + std::fflush(file); + } +} + +template +void log(std::format_string format, Args&&... args) +{ + write_log(std::format(format, std::forward(args)...)); +} + +// Diligent's messages (shader compilation errors, invalid states, ...), into the log. +void DILIGENT_CALL_TYPE diligent_message(DEBUG_MESSAGE_SEVERITY severity, const Char* message, const Char* function, + const Char*, int) +{ + static constexpr const char* severities[] = {"info", "warning", "error", "fatal error"}; + log("Diligent {}: {}{}{}", severities[severity < 4 ? severity : 3], function != nullptr ? function : "", + function != nullptr ? ": " : "", message != nullptr ? message : ""); +} struct Arguments { std::wstring channel; @@ -67,7 +99,18 @@ HWND create_child_window(HWND parent) .lpszClassName = L"T5DERenderer"}; RegisterClassExW(&windowClass); return CreateWindowExW(0, windowClass.lpszClassName, L"", WS_CHILD | WS_VISIBLE | WS_DISABLED | WS_CLIPSIBLINGS, - boxX, boxY, boxWidth, boxHeight, parent, nullptr, windowClass.hInstance, nullptr); + 0, 0, 1, 1, parent, nullptr, windowClass.hInstance, nullptr); +} + +// Where the child window goes in the client's 3D view. +RECT placement(HWND parent, View view) +{ + RECT client{}; + GetClientRect(parent, &client); + if (view == View::Split) { + client.left = client.right / 2; + } + return client; } } // namespace @@ -80,37 +123,65 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) } HANDLE mapping = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, arguments.channel.c_str()); auto* channel = mapping == nullptr ? nullptr - : static_cast(MapViewOfFile( - mapping, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(t5de::protocol::Channel))); - HANDLE frameEvent = OpenEventW(SYNCHRONIZE, FALSE, (arguments.channel + t5de::protocol::frameEventSuffix).c_str()); + : static_cast(MapViewOfFile(mapping, FILE_MAP_ALL_ACCESS, 0, 0, 0)); + HANDLE ready = OpenEventW(SYNCHRONIZE, FALSE, (arguments.channel + t5de::protocol::readyEventSuffix).c_str()); + HANDLE consumed = + OpenEventW(EVENT_MODIFY_STATE, FALSE, (arguments.channel + t5de::protocol::consumedEventSuffix).c_str()); HANDLE parent = OpenProcess(SYNCHRONIZE, FALSE, arguments.parent); - if (channel == nullptr || frameEvent == nullptr || parent == nullptr || channel->version != t5de::protocol::version) { + if (channel == nullptr || ready == nullptr || consumed == nullptr || parent == nullptr + || channel->version != t5de::protocol::version) { return 3; } + const auto* data = reinterpret_cast(channel + 1); - HWND window = create_child_window(reinterpret_cast(static_cast(channel->window))); + const HWND clientWindow = reinterpret_cast(static_cast(channel->window)); + const HWND window = create_child_window(clientWindow); if (window == nullptr) { return 4; } + log("started for window {:#x}, view {}", channel->window, static_cast(channel->view)); IEngineFactoryD3D11* factory = GetEngineFactoryD3D11(); + factory->SetMessageCallback(diligent_message); RefCntAutoPtr device; RefCntAutoPtr context; EngineD3D11CreateInfo engineInfo; factory->CreateDeviceAndContextsD3D11(engineInfo, &device, &context); if (!device) { + log("can't create a Direct3D 11 device"); return 5; } RefCntAutoPtr swapChain; SwapChainDesc swapChainDesc; + swapChainDesc.ColorBufferFormat = TEX_FORMAT_RGBA8_UNORM; // no sRGB conversion, like the client's renderer + swapChainDesc.DepthBufferFormat = TEX_FORMAT_D32_FLOAT; factory->CreateSwapChainD3D11(device, context, swapChainDesc, FullScreenModeDesc{}, Win32NativeWindow{window}, &swapChain); if (!swapChain) { + log("can't create a swap chain"); return 6; } + t5de::Scene scene(device, context, swapChain); + // Takes the chunk waiting in the channel (after "ready"), and lets the adapter send the next: appends it to + // frame, and returns whether it ends a frame. + std::vector frame; + const auto take_chunk = [&] { + const std::uint32_t size = channel->chunkSize <= channel->capacity ? channel->chunkSize : 0; + const bool endsFrame = (channel->chunkFlags & t5de::protocol::chunkEndsFrame) != 0; + frame.insert(frame.end(), data, data + size); + SetEvent(consumed); + return endsFrame; + }; + + LARGE_INTEGER frequency; + QueryPerformanceFrequency(&frequency); + double drawTime = 0; + int drawn = 0; + int skipped = 0; + RECT placed{}; for (;;) { - const HANDLE handles[] = {frameEvent, parent}; + const HANDLE handles[] = {ready, parent}; const DWORD result = MsgWaitForMultipleObjects(2, handles, FALSE, 1000, QS_ALLINPUT); MSG message; while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) { @@ -119,19 +190,55 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) if (result == WAIT_OBJECT_0 + 1 || channel->closing != 0) { break; // the client exited, or the engine is gone } - if (result != WAIT_OBJECT_0) { + if (result != WAIT_OBJECT_0 || !take_chunk()) { continue; } - // T5DE gold, pulsing with the client's frames. - const float pulse = 0.5f + 0.5f * std::sin(static_cast(channel->frames) * 0.1f); - const float color[] = {1.0f, 0.82f * pulse + 0.1f, 0.0f, 1.0f}; - ITextureView* backBuffer = swapChain->GetCurrentBackBufferRTV(); - context->SetRenderTargets(1, &backBuffer, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION); - context->ClearRenderTarget(backBuffer, color, RESOURCE_STATE_TRANSITION_MODE_TRANSITION); - swapChain->Present(0); + // If the client has sent more frames meanwhile (drawing fell behind), catch up: carry out the older ones' + // changes without drawing them, and draw only the newest. (An incomplete frame stays for the next round.) + std::size_t complete = frame.size(); + while (WaitForSingleObject(ready, 0) == WAIT_OBJECT_0) { + bool ended = take_chunk(); + while (!ended && WaitForSingleObject(ready, 1000) == WAIT_OBJECT_0) { + ended = take_chunk(); + } + if (!ended) { + break; + } + scene.execute(std::span(frame).first(complete), 0, false); + frame.erase(frame.begin(), frame.begin() + static_cast(complete)); + complete = frame.size(); + ++skipped; + } + + // Follow the client's 3D view's size. + const RECT place = placement(clientWindow, 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)); + placed = place; + log("drawing {}x{} at {} in the client's 3D view", width, height, place.left); + } + LARGE_INTEGER start, end; + QueryPerformanceCounter(&start); + if (scene.execute(std::span(frame).first(complete), place.left, width > 0 && height > 0)) { + swapChain->Present(0); + QueryPerformanceCounter(&end); + drawTime += static_cast(end.QuadPart - start.QuadPart) / static_cast(frequency.QuadPart); + if (++drawn == 300) { + log("last 300 frames: {:.1f} ms each to draw and present, {} skipped to catch up", drawTime * 1000 / drawn, + skipped); + drawTime = 0; + drawn = 0; + skipped = 0; + } + } + frame.erase(frame.begin(), frame.begin() + static_cast(complete)); } + log("exiting"); DestroyWindow(window); return 0; } diff --git a/renderer/scene.cpp b/renderer/scene.cpp new file mode 100644 index 0000000..fc5b54b --- /dev/null +++ b/renderer/scene.cpp @@ -0,0 +1,612 @@ +#include "scene.hpp" + +#include +#include + +#include "fixed_function_hlsl.hpp" + +namespace t5de { + +using namespace Diligent; +using protocol::Op; + +namespace { + +void set_identity(float (&m)[16]) +{ + std::memset(m, 0, sizeof(m)); + m[0] = m[5] = m[10] = m[15] = 1; +} + +// An affine transform (3x3 row-major, then translation) as a 4x4 matrix for row vectors. +void set_transform(float (&m)[16], const float (&t)[12]) +{ + for (int row = 0; row < 3; ++row) { + m[row * 4 + 0] = t[row * 3 + 0]; + m[row * 4 + 1] = t[row * 3 + 1]; + m[row * 4 + 2] = t[row * 3 + 2]; + m[row * 4 + 3] = 0; + } + m[12] = t[9]; + m[13] = t[10]; + m[14] = t[11]; + m[15] = 1; +} + +// u' = u * m[0] + v * m[1] + m[4], v' = u * m[2] + v * m[3] + m[5], as two rows dotted with (u, v, 1). +void set_texture_transform(float (&rows)[8], const float (&m)[6]) +{ + const float values[8] = {m[0], m[1], m[4], 0, m[2], m[3], m[5], 0}; + std::memcpy(rows, values, sizeof(rows)); +} + +void set_color(float (&rgba)[4], std::uint32_t color) +{ + rgba[0] = static_cast((color >> 16) & 0xff) / 255.0f; + rgba[1] = static_cast((color >> 8) & 0xff) / 255.0f; + rgba[2] = static_cast(color & 0xff) / 255.0f; + rgba[3] = static_cast(color >> 24) / 255.0f; +} + +constexpr std::uint32_t hasNormals = 1; +constexpr std::uint32_t hasTexCoords = 2; +constexpr std::uint32_t hasColors = 4; + +} // namespace + +Scene::Scene(IRenderDevice* device, IDeviceContext* context, ISwapChain* swapChain) + : device_(device), context_(context), swapChain_(swapChain) +{ + BufferDesc constants; + constants.Name = "Constants"; + constants.Size = sizeof(Constants); + constants.Usage = USAGE_DYNAMIC; + constants.BindFlags = BIND_UNIFORM_BUFFER; + constants.CPUAccessFlags = CPU_ACCESS_WRITE; + device_->CreateBuffer(constants, nullptr, &constantBuffer_); + + // IMVU's textures: bilinear, no mipmaps, wrapped or clamped per texture. + for (int wrap = 0; wrap < 4; ++wrap) { + SamplerDesc sampler; + sampler.MinFilter = FILTER_TYPE_LINEAR; + sampler.MagFilter = FILTER_TYPE_LINEAR; + sampler.MipFilter = FILTER_TYPE_POINT; + sampler.AddressU = (wrap & 1) != 0 ? TEXTURE_ADDRESS_WRAP : TEXTURE_ADDRESS_CLAMP; + sampler.AddressV = (wrap & 2) != 0 ? TEXTURE_ADDRESS_WRAP : TEXTURE_ADDRESS_CLAMP; + device_->CreateSampler(sampler, &samplers_[wrap]); + } + + set_identity(constants_.world); + set_identity(constants_.view); + set_identity(constants_.projection); + const float identity[6] = {1, 0, 0, 1, 0, 0}; + set_texture_transform(constants_.texTransform0, identity); + set_texture_transform(constants_.texTransform1, identity); + constants_.flags[2] = 127; // the client's alpha reference +} + +bool Scene::execute(std::span commands, int offsetX, bool draw) +{ + offsetX_ = offsetX; + draw_ = draw; + bool drew = false; + protocol::Reader reader(commands); + while (const auto command = reader.next()) { + switch (command->op()) { + case Op::BeginFrame: + if (const auto begin = command->body()) { + onscreen_ = begin->offscreen == 0 && draw_; + if (onscreen_) { + ITextureView* target = swapChain_->GetCurrentBackBufferRTV(); + context_->SetRenderTargets(1, &target, swapChain_->GetDepthBufferDSV(), + RESOURCE_STATE_TRANSITION_MODE_TRANSITION); + } + } + break; + case Op::EndFrame: + drew = drew || onscreen_; + onscreen_ = false; + break; + case Op::Clear: + if (const auto clear = command->body(); clear && onscreen_) { + if ((clear->flags & 1) != 0) { + float color[4]; + set_color(color, clear->color); + context_->ClearRenderTarget(swapChain_->GetCurrentBackBufferRTV(), color, + RESOURCE_STATE_TRANSITION_MODE_TRANSITION); + } + if ((clear->flags & 2) != 0) { + context_->ClearDepthStencil(swapChain_->GetDepthBufferDSV(), CLEAR_DEPTH_FLAG, 1.0f, 0, + RESOURCE_STATE_TRANSITION_MODE_TRANSITION); + } + } + break; + case Op::SetProjection: + if (const auto projection = command->body()) { + setProjection(*projection); + } + break; + case Op::SetModel: + if (const auto model = command->body()) { + set_transform(constants_.world, model->m); + } + break; + case Op::SetView: + if (const auto view = command->body()) { + set_transform(constants_.view, view->m); + } + break; + case Op::SetTextures: + if (const auto textures = command->body()) { + state_.texture0 = textures->texture0; + state_.texture1 = textures->texture1; + } + break; + case Op::SetState: + if (const auto state = command->body()) { + setState(*state); + } + break; + case Op::SetLight: + if (const auto light = command->body(); light && light->index < 8) { + set_color(constants_.lightColor[light->index], light->color); + constants_.lightColor[light->index][3] = light->enabled != 0 ? 1.0f : 0.0f; + std::memcpy(constants_.lightVector[light->index], light->vector, sizeof(light->vector)); + constants_.lightVector[light->index][3] = light->directional != 0 ? 1.0f : 0.0f; + } + break; + case Op::SetFog: + if (const auto fog = command->body()) { + constants_.fog[0] = fog->start; + constants_.fog[1] = fog->end; + set_color(constants_.fogColor, fog->color); + } + break; + case Op::SetTextureTransform: + if (const auto transform = command->body(); transform && transform->stage < 2) { + set_texture_transform(transform->stage == 0 ? constants_.texTransform0 : constants_.texTransform1, + transform->m); + } + break; + case Op::CreateTexture: + createTexture(*command); + break; + case Op::UpdateTexture: + updateTexture(*command); + break; + case Op::DestroyTexture: + if (const auto destroy = command->body()) { + textures_.erase(destroy->id); + } + break; + case Op::CreateBuffer: + createBuffer(*command); + break; + case Op::UpdateBuffer: + updateBuffer(*command); + break; + case Op::DestroyBuffer: + if (const auto destroy = command->body()) { + buffers_.erase(destroy->id); + } + break; + case Op::DrawTriangles: + if (const auto triangles = command->body(); triangles && onscreen_) { + drawTriangles(*triangles); + } + break; + case Op::DrawArrays: + if (onscreen_) { + drawArrays(*command); + } + break; + } + } + return drew; +} + +void Scene::setProjection(const protocol::SetProjection& projection) +{ + std::memcpy(constants_.projection, projection.m, sizeof(constants_.projection)); + if (!onscreen_) { + return; + } + // The client's projection covers its whole 3D view; the window shows part of it, from offsetX. + const SwapChainDesc& window = swapChain_->GetDesc(); + Viewport viewport; + viewport.TopLeftX = static_cast(-offsetX_); + viewport.TopLeftY = 0; + viewport.Width = static_cast(projection.width); + viewport.Height = static_cast(projection.height); + viewport.MinDepth = 0; + viewport.MaxDepth = 1; + context_->SetViewports(1, &viewport, window.Width, window.Height); +} + +void Scene::setState(const protocol::SetState& state) +{ + const bool on = state.value != 0; + switch (state.state) { + case protocol::State::DepthTest: + state_.depthTest = on; + break; + case protocol::State::DepthWrite: + state_.depthWrite = on; + break; + case protocol::State::Blending: + state_.blending = on; + break; + case protocol::State::BlendMode: + if (state.value >= 1 && state.value <= 4) { + state_.blendMode = state.value; + } + break; + case protocol::State::AlphaTest: + constants_.flags[1] = on ? 1.0f : 0.0f; + break; + case protocol::State::AlphaReference: + constants_.flags[2] = static_cast(state.value); + break; + case protocol::State::Culling: + state_.culling = on; + break; + case protocol::State::NormalInverting: + state_.normalInverting = on; + break; + case protocol::State::Lighting: + constants_.flags[0] = on ? 1.0f : 0.0f; + break; + case protocol::State::Fog: + constants_.fog[2] = on ? 1.0f : 0.0f; + break; + case protocol::State::Ambient: + set_color(constants_.ambient, state.value); + break; + } +} + +void Scene::createTexture(const protocol::Command& command) +{ + const auto create = command.body(); + const auto pixels = command.payload(); + if (!create || create->width == 0 || create->height == 0 + || pixels.size() < std::size_t{create->width} * create->height * 4) { + return; + } + TextureDesc desc; + desc.Name = "Client texture"; + desc.Type = RESOURCE_DIM_TEX_2D; + desc.Width = create->width; + desc.Height = create->height; + desc.MipLevels = 1; + desc.Format = TEX_FORMAT_BGRA8_UNORM; // 0xAARRGGBB in memory + desc.BindFlags = BIND_SHADER_RESOURCE; + desc.Usage = USAGE_DEFAULT; + TextureSubResData level; + level.pData = pixels.data(); + level.Stride = std::uint64_t{create->width} * 4; + TextureData data; + data.pSubResources = &level; + data.NumSubresources = 1; + Texture texture{.width = create->width, .height = create->height, .flags = create->flags}; + device_->CreateTexture(desc, &data, &texture.texture); + if (!texture.texture) { + return; + } + texture.texture->GetDefaultView(TEXTURE_VIEW_SHADER_RESOURCE) + ->SetSampler(samplers_[((create->flags & 2) != 0 ? 1 : 0) | ((create->flags & 4) != 0 ? 2 : 0)]); + textures_[create->id] = std::move(texture); +} + +void Scene::updateTexture(const protocol::Command& command) +{ + const auto update = command.body(); + const auto pixels = command.payload(); + if (!update || pixels.size() < std::size_t{update->width} * update->height * 4) { + return; + } + const auto found = textures_.find(update->id); + if (found == textures_.end()) { + return; + } + Texture& texture = found->second; + if (texture.width != update->width || texture.height != update->height) { + // A different size: make it again, keeping its sampling. + protocol::Writer writer; + std::memcpy(writer.add(Op::CreateTexture, + protocol::CreateTexture{update->id, update->width, update->height, texture.flags}, + pixels.size()), + pixels.data(), pixels.size()); + protocol::Reader reader(writer.bytes()); + createTexture(*reader.next()); + return; + } + Box box; + box.MaxX = update->width; + box.MaxY = update->height; + TextureSubResData level; + level.pData = pixels.data(); + level.Stride = std::uint64_t{update->width} * 4; + context_->UpdateTexture(texture.texture, 0, 0, box, level, RESOURCE_STATE_TRANSITION_MODE_TRANSITION, + RESOURCE_STATE_TRANSITION_MODE_TRANSITION); +} + +void Scene::createBuffer(const protocol::Command& command) +{ + const auto create = command.body(); + if (!create || create->count == 0 || create->elementSize == 0) { + return; + } + const std::size_t size = std::size_t{create->elementSize} * create->count; + const auto contents = command.payload(); + const bool filled = contents.size() >= size; + BufferDesc desc; + desc.Name = create->kind == protocol::BufferKind::Index ? "Client indices" : "Client vertices"; + desc.Size = size; + desc.BindFlags = create->kind == protocol::BufferKind::Index ? BIND_INDEX_BUFFER : BIND_VERTEX_BUFFER; + desc.Usage = filled ? USAGE_IMMUTABLE : USAGE_DEFAULT; // dynamic buffers are filled by updates + BufferData data; + data.pData = contents.data(); + data.DataSize = size; + Buffer buffer{.kind = create->kind, .elementSize = create->elementSize, .count = create->count}; + device_->CreateBuffer(desc, filled ? &data : nullptr, &buffer.buffer); + if (buffer.buffer) { + buffers_[create->id] = std::move(buffer); + } +} + +void Scene::updateBuffer(const protocol::Command& command) +{ + const auto update = command.body(); + const auto contents = command.payload(); + if (!update) { + return; + } + const auto found = buffers_.find(update->id); + const std::size_t size = std::size_t{update->elementSize} * update->count; + if (found == buffers_.end() || contents.size() < size || found->second.elementSize != update->elementSize + || found->second.count != update->count || found->second.buffer->GetDesc().Usage != USAGE_DEFAULT) { + return; + } + context_->UpdateBuffer(found->second.buffer, 0, size, contents.data(), RESOURCE_STATE_TRANSITION_MODE_TRANSITION); +} + +ITextureView* Scene::textureView(std::uint32_t id) +{ + const auto found = textures_.find(id); + return found == textures_.end() ? nullptr : found->second.texture->GetDefaultView(TEXTURE_VIEW_SHADER_RESOURCE); +} + +void Scene::drawTriangles(const protocol::DrawTriangles& draw) +{ + const auto buffer = [this](std::uint32_t id, protocol::BufferKind kind, std::uint32_t elementSize) -> Buffer* { + const auto found = buffers_.find(id); + if (id == 0 || found == buffers_.end() || found->second.kind != kind || found->second.elementSize != elementSize) { + return nullptr; + } + return &found->second; + }; + Buffer* indices = buffer(draw.indices, protocol::BufferKind::Index, 2); + Buffer* positions = buffer(draw.positions, protocol::BufferKind::Vertex, 12); + if (indices == nullptr || positions == nullptr) { + return; + } + // Optional streams count only if they cover every vertex. + const auto optional = [&](std::uint32_t id, std::uint32_t elementSize) -> IBuffer* { + Buffer* found = buffer(id, protocol::BufferKind::Vertex, elementSize); + return found != nullptr && found->count >= positions->count ? found->buffer.RawPtr() : nullptr; + }; + this->draw(indices->buffer, indices->count, positions->buffer, optional(draw.normals, 12), + optional(draw.texCoords, 8), optional(draw.colors, 4)); +} + +void Scene::drawArrays(const protocol::Command& command) +{ + const auto arrays = command.body(); + const auto payload = command.payload(); + if (!arrays || arrays->vertexCount == 0 || arrays->indexCount == 0) { + return; + } + const std::size_t indexBytes = protocol::padded(std::size_t{arrays->indexCount} * 2); + const std::size_t vertices = arrays->vertexCount; + if (payload.size() < indexBytes + vertices * (12 + (arrays->hasNormals ? 12 : 0) + (arrays->hasTexCoords ? 8 : 0) + + (arrays->hasColors ? 4 : 0))) { + return; + } + const auto make = [this](const std::byte* data, std::size_t size, BIND_FLAGS bind) { + BufferDesc desc; + desc.Name = "Client arrays"; + desc.Size = size; + desc.BindFlags = bind; + desc.Usage = USAGE_IMMUTABLE; + BufferData contents; + contents.pData = data; + contents.DataSize = size; + RefCntAutoPtr buffer; + device_->CreateBuffer(desc, &contents, &buffer); + return buffer; + }; + const std::byte* next = payload.data(); + const auto indices = make(next, std::size_t{arrays->indexCount} * 2, BIND_INDEX_BUFFER); + next += indexBytes; + const auto positions = make(next, vertices * 12, BIND_VERTEX_BUFFER); + next += vertices * 12; + RefCntAutoPtr normals, texCoords, colors; + if (arrays->hasNormals) { + normals = make(next, vertices * 12, BIND_VERTEX_BUFFER); + next += vertices * 12; + } + if (arrays->hasTexCoords) { + texCoords = make(next, vertices * 8, BIND_VERTEX_BUFFER); + next += vertices * 8; + } + if (arrays->hasColors) { + colors = make(next, vertices * 4, BIND_VERTEX_BUFFER); + } + draw(indices, arrays->indexCount, positions, normals, texCoords, colors); +} + +void Scene::draw(IBuffer* indices, std::uint32_t indexCount, IBuffer* positions, IBuffer* normals, IBuffer* texCoords, + IBuffer* colors) +{ + if (indices == nullptr || positions == nullptr || indexCount == 0) { + return; + } + // Like the client: no textures, the first, or both (the second only with a first). + ITextureView* texture0 = textureView(state_.texture0); + ITextureView* texture1 = texture0 != nullptr ? textureView(state_.texture1) : nullptr; + const std::uint32_t textures = texture0 == nullptr ? 0 : texture1 == nullptr ? 1 : 2; + const std::uint32_t streams = + (normals != nullptr ? hasNormals : 0) | (texCoords != nullptr ? hasTexCoords : 0) | (colors != nullptr ? hasColors : 0); + Pipeline* selected = pipeline(streams, textures); + if (selected == nullptr) { + return; + } + + void* mapped = nullptr; + context_->MapBuffer(constantBuffer_, MAP_WRITE, MAP_FLAG_DISCARD, mapped); + if (mapped == nullptr) { + return; + } + std::memcpy(mapped, &constants_, sizeof(constants_)); + context_->UnmapBuffer(constantBuffer_, MAP_WRITE); + + if (selected->texture0 != nullptr) { + selected->texture0->Set(texture0); + } + if (selected->texture1 != nullptr) { + selected->texture1->Set(texture1); + } + context_->SetPipelineState(selected->state); + context_->CommitShaderResources(selected->bindings, RESOURCE_STATE_TRANSITION_MODE_TRANSITION); + + IBuffer* vertexBuffers[4]; + std::uint32_t count = 0; + for (IBuffer* stream : {positions, normals, texCoords, colors}) { + if (stream != nullptr) { + vertexBuffers[count++] = stream; + } + } + context_->SetVertexBuffers(0, count, vertexBuffers, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION, + SET_VERTEX_BUFFERS_FLAG_RESET); + context_->SetIndexBuffer(indices, 0, RESOURCE_STATE_TRANSITION_MODE_TRANSITION); + DrawIndexedAttribs attributes; + attributes.NumIndices = indexCount; + attributes.IndexType = VT_UINT16; + context_->DrawIndexed(attributes); +} + +const Scene::Shaders& Scene::shaders(std::uint32_t streams, std::uint32_t textures) +{ + const std::uint32_t key = streams | (textures << 3); + if (const auto found = shaders_.find(key); found != shaders_.end()) { + return found->second; + } + 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; + Shaders created; + ShaderCreateInfo info; + info.SourceLanguage = SHADER_SOURCE_LANGUAGE_HLSL; + info.Desc.UseCombinedTextureSamplers = true; // g_TextureN with g_TextureN_sampler, set from each texture's view + info.Source = source.c_str(); + info.Desc.ShaderType = SHADER_TYPE_VERTEX; + info.Desc.Name = "Fixed function (vertex)"; + info.EntryPoint = "vs_main"; + device_->CreateShader(info, &created.vertex); + info.Desc.ShaderType = SHADER_TYPE_PIXEL; + info.Desc.Name = "Fixed function (pixel)"; + info.EntryPoint = "ps_main"; + device_->CreateShader(info, &created.pixel); + return shaders_[key] = std::move(created); +} + +Scene::Pipeline* Scene::pipeline(std::uint32_t streams, std::uint32_t textures) +{ + const std::uint32_t blend = state_.blending ? state_.blendMode : 0; + const std::uint32_t key = streams | (textures << 3) | (blend << 5) | (state_.depthTest ? 1u << 8 : 0) | + (state_.depthWrite ? 1u << 9 : 0) | (state_.culling ? 1u << 10 : 0) | + (state_.normalInverting ? 1u << 11 : 0); + if (const auto found = pipelines_.find(key); found != pipelines_.end()) { + return found->second.state ? &found->second : nullptr; + } + Pipeline& entry = pipelines_[key]; + const Shaders& compiled = shaders(streams, textures); + if (!compiled.vertex || !compiled.pixel) { + return nullptr; + } + + GraphicsPipelineStateCreateInfo info; + info.PSODesc.Name = "Fixed function"; + info.PSODesc.PipelineType = PIPELINE_TYPE_GRAPHICS; + const SwapChainDesc& window = swapChain_->GetDesc(); + GraphicsPipelineDesc& graphics = info.GraphicsPipeline; + graphics.NumRenderTargets = 1; + graphics.RTVFormats[0] = window.ColorBufferFormat; + graphics.DSVFormat = window.DepthBufferFormat; + graphics.PrimitiveTopology = PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + // The client culls clockwise triangles, or counter-clockwise ones when normals are inverted (mirrored objects). + graphics.RasterizerDesc.CullMode = state_.culling ? CULL_MODE_BACK : CULL_MODE_NONE; + graphics.RasterizerDesc.FrontCounterClockwise = !state_.normalInverting; + graphics.DepthStencilDesc.DepthEnable = state_.depthTest; + graphics.DepthStencilDesc.DepthWriteEnable = state_.depthWrite; + graphics.DepthStencilDesc.DepthFunc = COMPARISON_FUNC_LESS; + RenderTargetBlendDesc& blending = graphics.BlendDesc.RenderTargets[0]; + blending.BlendEnable = blend != 0; + if (blend != 0) { + const BLEND_FACTOR source = blend == 1 || blend == 4 ? BLEND_FACTOR_SRC_ALPHA : BLEND_FACTOR_ONE; + const BLEND_FACTOR destination = blend == 2 || blend == 4 ? BLEND_FACTOR_ONE : BLEND_FACTOR_INV_SRC_ALPHA; + blending.SrcBlend = blending.SrcBlendAlpha = source; + blending.DestBlend = blending.DestBlendAlpha = destination; + blending.BlendOp = blending.BlendOpAlpha = BLEND_OPERATION_ADD; + } + + LayoutElement elements[4]; + std::uint32_t count = 0; + elements[count] = LayoutElement{0, count, 3, VT_FLOAT32, False}; + ++count; + if ((streams & hasNormals) != 0) { + elements[count] = LayoutElement{1, count, 3, VT_FLOAT32, False}; + ++count; + } + if ((streams & hasTexCoords) != 0) { + elements[count] = LayoutElement{2, count, 2, VT_FLOAT32, False}; + ++count; + } + if ((streams & hasColors) != 0) { + elements[count] = LayoutElement{3, count, 4, VT_UINT8, True}; + ++count; + } + graphics.InputLayout.LayoutElements = elements; + graphics.InputLayout.NumElements = count; + + info.pVS = compiled.vertex; + info.pPS = compiled.pixel; + const ShaderResourceVariableDesc variables[] = { + {SHADER_TYPE_PIXEL, "g_Texture0", SHADER_RESOURCE_VARIABLE_TYPE_DYNAMIC}, + {SHADER_TYPE_PIXEL, "g_Texture1", SHADER_RESOURCE_VARIABLE_TYPE_DYNAMIC}, + }; + info.PSODesc.ResourceLayout.DefaultVariableType = SHADER_RESOURCE_VARIABLE_TYPE_STATIC; + info.PSODesc.ResourceLayout.Variables = variables; + info.PSODesc.ResourceLayout.NumVariables = textures; + device_->CreateGraphicsPipelineState(info, &entry.state); + if (!entry.state) { + return nullptr; + } + for (const SHADER_TYPE stage : {SHADER_TYPE_VERTEX, SHADER_TYPE_PIXEL}) { + if (IShaderResourceVariable* constants = entry.state->GetStaticVariableByName(stage, "Constants")) { + constants->Set(constantBuffer_); + } + } + entry.state->CreateShaderResourceBinding(&entry.bindings, true); + if (textures >= 1) { + entry.texture0 = entry.bindings->GetVariableByName(SHADER_TYPE_PIXEL, "g_Texture0"); + } + if (textures >= 2) { + entry.texture1 = entry.bindings->GetVariableByName(SHADER_TYPE_PIXEL, "g_Texture1"); + } + return &entry; +} + +} // namespace t5de diff --git a/renderer/scene.hpp b/renderer/scene.hpp new file mode 100644 index 0000000..092970c --- /dev/null +++ b/renderer/scene.hpp @@ -0,0 +1,114 @@ +#pragma once +// Carries out the command stream (include/t5de/protocol/commands.hpp) with Diligent, reproducing IMVU's Direct3D 9 +// look (shaders/fixed_function.hlsl): the client's textures and buffers by id, its state as it sets it, a pipeline +// for each combination of states it draws with. + +#include +#include +#include +#include + +#include "EngineFactoryD3D11.h" +#include "RefCntAutoPtr.hpp" +#include "t5de/protocol/commands.hpp" + +namespace t5de { + +class Scene { +public: + Scene(Diligent::IRenderDevice* device, Diligent::IDeviceContext* context, Diligent::ISwapChain* swapChain); + + // Carries out one frame's commands. offsetX is where the window starts within the client's 3D view; without draw, + // only the frame's changes are made (textures, buffers, state). True if the frame was drawn into the window (to + // present). + bool execute(std::span commands, int offsetX, bool draw); + +private: + struct Texture { + Diligent::RefCntAutoPtr texture; + std::uint32_t width = 0; + std::uint32_t height = 0; + std::uint32_t flags = 0; + }; + + struct Buffer { + Diligent::RefCntAutoPtr buffer; + protocol::BufferKind kind = protocol::BufferKind::Vertex; + std::uint32_t elementSize = 0; + std::uint32_t count = 0; + }; + + struct Pipeline { + Diligent::RefCntAutoPtr state; + Diligent::RefCntAutoPtr bindings; + Diligent::IShaderResourceVariable* texture0 = nullptr; + Diligent::IShaderResourceVariable* texture1 = nullptr; + }; + + struct Shaders { + Diligent::RefCntAutoPtr vertex; + Diligent::RefCntAutoPtr pixel; + }; + + // Mirrors the shader's constant buffer. + struct Constants { + float world[16]; + float view[16]; + float projection[16]; + float texTransform0[8]; + float texTransform1[8]; + float ambient[4]; + float lightColor[8][4]; + float lightVector[8][4]; + float fogColor[4]; + float fog[4]; + float flags[4]; + }; + + // What a draw needs from the state, as set by the client. + struct State { + bool depthTest = true; + bool depthWrite = true; + bool blending = false; + std::uint32_t blendMode = 1; + bool culling = true; + bool normalInverting = false; + std::uint32_t texture0 = 0; + std::uint32_t texture1 = 0; + }; + + void createTexture(const protocol::Command& command); + void updateTexture(const protocol::Command& command); + void createBuffer(const protocol::Command& command); + void updateBuffer(const protocol::Command& command); + void setProjection(const protocol::SetProjection& projection); + void setState(const protocol::SetState& state); + void drawTriangles(const protocol::DrawTriangles& draw); + void drawArrays(const protocol::Command& command); + + // Draws indexCount indices from index, with vertex streams (positions required, the others may be null). + void draw(Diligent::IBuffer* indices, std::uint32_t indexCount, Diligent::IBuffer* positions, + Diligent::IBuffer* normals, Diligent::IBuffer* texCoords, Diligent::IBuffer* colors); + Pipeline* pipeline(std::uint32_t streams, std::uint32_t textures); + const Shaders& shaders(std::uint32_t streams, std::uint32_t textures); + Diligent::ITextureView* textureView(std::uint32_t id); + + Diligent::RefCntAutoPtr device_; + Diligent::RefCntAutoPtr context_; + Diligent::RefCntAutoPtr swapChain_; + Diligent::RefCntAutoPtr constantBuffer_; + Diligent::RefCntAutoPtr samplers_[4]; // by wrap u (1) and wrap v (2) + + std::unordered_map textures_; + std::unordered_map buffers_; + std::unordered_map shaders_; + std::unordered_map pipelines_; + + Constants constants_{}; + State state_; + bool onscreen_ = false; // in a frame drawn into the window + bool draw_ = true; + int offsetX_ = 0; +}; + +} // namespace t5de diff --git a/renderer/shaders/embed.hpp.in b/renderer/shaders/embed.hpp.in new file mode 100644 index 0000000..96d719f --- /dev/null +++ b/renderer/shaders/embed.hpp.in @@ -0,0 +1,4 @@ +#pragma once +// Generated by CMake from renderer/shaders/@T5DE_SHADER_FILE@: the shader source, compiled at run time by Diligent. + +inline constexpr const char* @T5DE_SHADER_NAME@ = R"hlsl(@T5DE_SHADER_SOURCE@)hlsl"; diff --git a/renderer/shaders/fixed_function.hlsl b/renderer/shaders/fixed_function.hlsl new file mode 100644 index 0000000..8808e57 --- /dev/null +++ b/renderer/shaders/fixed_function.hlsl @@ -0,0 +1,117 @@ +// IMVU's Direct3D 9 fixed-function look, as its renderer sets it up (docs/interface.md): +// - per-vertex (Gouraud) lighting, with the vertex color as diffuse and ambient material (white without one): the +// environment's ambient light, plus each light's color times max(0, N.L); no specular or emissive light; +// - textures: none (the lit color), one (times the lit color), or two (the second blended over the first by its +// alpha, then times the lit color), both on the vertex's texture coordinates with their own transforms; +// - an alpha test passing pixels whose alpha is greater than the reference, then linear fog by view depth. +// +// Compiled for each combination of the vertex streams present (HAS_NORMALS, HAS_TEX_COORDS, HAS_COLORS) and the +// number of textures (TEXTURES). + +cbuffer Constants +{ + row_major float4x4 g_World; + row_major float4x4 g_View; + row_major float4x4 g_Projection; + float4 g_TexTransform0[2]; // u' = dot((u, v, 1), [0].xyz), v' = dot((u, v, 1), [1].xyz) + float4 g_TexTransform1[2]; + float4 g_Ambient; // rgb + float4 g_LightColor[8]; // rgb, a: enabled + float4 g_LightVector[8]; // xyz: position, or direction toward the light; w: 1 if directional + float4 g_FogColor; // rgb + float4 g_Fog; // x: start, y: end, z: on + float4 g_Flags; // x: lighting on, y: alpha test on, z: alpha reference (0-255) +}; + +struct VSInput +{ + float3 Position : ATTRIB0; +#if HAS_NORMALS + float3 Normal : ATTRIB1; +#endif +#if HAS_TEX_COORDS + float2 TexCoord : ATTRIB2; +#endif +#if HAS_COLORS + float4 Color : ATTRIB3; // bytes B, G, R, A +#endif +}; + +struct PSInput +{ + float4 Position : SV_POSITION; + float4 Color : COLOR0; + float2 TexCoord0 : TEXCOORD0; + float2 TexCoord1 : TEXCOORD1; + float Depth : TEXCOORD2; +}; + +void vs_main(in VSInput input, out PSInput output) +{ + float4 world = mul(float4(input.Position, 1.0), g_World); + float4 view = mul(world, g_View); + output.Position = mul(view, g_Projection); + output.Depth = abs(view.z); + + float4 material = float4(1.0, 1.0, 1.0, 1.0); +#if HAS_COLORS + material = input.Color.zyxw; +#endif + float4 color = material; + if (g_Flags.x > 0.5) + { + float3 normal = float3(0.0, 0.0, 0.0); +#if HAS_NORMALS + normal = normalize(mul(input.Normal, (float3x3)g_World)); +#endif + float3 lit = g_Ambient.rgb * material.rgb; + for (int i = 0; i < 8; ++i) + { + if (g_LightColor[i].a > 0.5) + { + float3 toLight = g_LightVector[i].w > 0.5 ? g_LightVector[i].xyz : g_LightVector[i].xyz - world.xyz; + lit += g_LightColor[i].rgb * material.rgb * max(0.0, dot(normal, normalize(toLight))); + } + } + color = float4(saturate(lit), material.a); + } + output.Color = color; + + float3 uv = float3(0.0, 0.0, 1.0); +#if HAS_TEX_COORDS + uv.xy = input.TexCoord; +#endif + output.TexCoord0 = float2(dot(uv, g_TexTransform0[0].xyz), dot(uv, g_TexTransform0[1].xyz)); + output.TexCoord1 = float2(dot(uv, g_TexTransform1[0].xyz), dot(uv, g_TexTransform1[1].xyz)); +} + +#if TEXTURES >= 1 +Texture2D g_Texture0; +SamplerState g_Texture0_sampler; +#endif +#if TEXTURES >= 2 +Texture2D g_Texture1; +SamplerState g_Texture1_sampler; +#endif + +float4 ps_main(in PSInput input) : SV_Target +{ + float4 color = input.Color; +#if TEXTURES == 1 + color *= g_Texture0.Sample(g_Texture0_sampler, input.TexCoord0); +#elif TEXTURES == 2 + float4 first = g_Texture0.Sample(g_Texture0_sampler, input.TexCoord0); + float4 second = g_Texture1.Sample(g_Texture1_sampler, input.TexCoord1); + color *= lerp(first, second, second.a); +#endif + if (g_Flags.y > 0.5 && !(color.a * 255.0 > g_Flags.z)) + { + discard; + } + if (g_Fog.z > 0.5) + { + float visible = saturate((g_Fog.y - input.Depth) / max(g_Fog.y - g_Fog.x, 1e-6)); + color.rgb = lerp(g_FogColor.rgb, color.rgb, visible); + } + return color; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index be8af7a..4e6962a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,3 +1,7 @@ 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) + +add_executable(protocol_test protocol_test.cpp) +target_link_libraries(protocol_test PRIVATE t5de::protocol t5de::options) +add_test(NAME protocol COMMAND protocol_test) diff --git a/tests/protocol_test.cpp b/tests/protocol_test.cpp new file mode 100644 index 0000000..45d114c --- /dev/null +++ b/tests/protocol_test.cpp @@ -0,0 +1,61 @@ +// The command stream: what the adapter writes, the renderer process reads back the same. + +#include +#include + +#include "t5de/protocol/commands.hpp" + +using namespace t5de::protocol; + +namespace { + +int failures = 0; + +void check(bool ok, const char* what) +{ + if (!ok) { + std::printf("FAIL: %s\n", what); + ++failures; + } +} + +} // namespace + +int main() +{ + Writer writer; + writer.add(Op::Clear, Clear{3, 0xff131313}); + const unsigned char pixels[] = {1, 2, 3, 4, 5, 6, 7, 8}; + std::memcpy(writer.add(Op::CreateTexture, CreateTexture{7, 2, 1, 6}, sizeof(pixels)), pixels, sizeof(pixels)); + std::byte* odd = writer.add(Op::UpdateBuffer, UpdateBuffer{9, 1, 3}, 3); // padded to 4 + std::memset(odd, 0xab, 3); + writer.add(Op::EndFrame, EndFrame{}); + check(writer.size() % 4 == 0, "commands stay 4-byte aligned"); + + Reader reader(writer.bytes()); + auto clear = reader.next(); + check(clear && clear->op() == Op::Clear && clear->body()->color == 0xff131313, "clear"); + auto texture = reader.next(); + check(texture && texture->op() == Op::CreateTexture && texture->body()->id == 7, "texture"); + check(texture && texture->payload().size() == sizeof(pixels) + && std::memcmp(texture->payload().data(), pixels, sizeof(pixels)) == 0, + "texture pixels"); + auto update = reader.next(); + check(update && update->payload().size() == 4, "payload padded"); + auto end = reader.next(); + check(end && end->op() == Op::EndFrame, "end of frame"); + check(!reader.next(), "nothing after"); + + // A truncated stream (a chunk cut short) yields only whole commands. + Reader truncated(writer.bytes().first(writer.size() - 2)); + int count = 0; + while (truncated.next()) { + ++count; + } + check(count == 3, "truncated: whole commands only"); + + if (failures == 0) { + std::printf("ok\n"); + } + return failures == 0 ? 0 : 1; +}