From a04dba18c10c653fba48bb0e6ff9af272454bcaa Mon Sep 17 00:00:00 2001 From: dhkatz Date: Wed, 28 Oct 2026 06:11:51 -0700 Subject: [PATCH] Draw particles, match the client's fog, and measure frame delay Particles (point sprites) are drawn as instanced camera-facing squares sized like Direct3D 9's. Fog is by depth buffer value, as Direct3D 9 does with IMVU's projections. At most one frame is queued for display, frames carry the adapter's timestamp so the renderer process logs the delay to presenting, and the log's lines from several processes no longer mix. Co-Authored-By: Claude Opus 5.5 --- adapter/engine.cpp | 19 +++- docs/architecture.md | 24 +++-- docs/interface.md | 7 +- include/t5de/imvu/types.hpp | 17 ++- include/t5de/protocol/commands.hpp | 9 +- renderer/CMakeLists.txt | 2 +- renderer/log.cpp | 30 ++++++ renderer/log.hpp | 18 ++++ renderer/main.cpp | 57 +++++----- renderer/scene.cpp | 150 ++++++++++++++++++++------- renderer/scene.hpp | 14 ++- renderer/shaders/fixed_function.hlsl | 36 ++++++- 12 files changed, 293 insertions(+), 90 deletions(-) create mode 100644 renderer/log.cpp create mode 100644 renderer/log.hpp diff --git a/adapter/engine.cpp b/adapter/engine.cpp index 8aeb4a2..21281ba 100644 --- a/adapter/engine.cpp +++ b/adapter/engine.cpp @@ -1,5 +1,7 @@ #include "engine.hpp" +#include + #include #include #include @@ -234,7 +236,9 @@ void Engine::beginRendering(imvu::PiRenderTarget* target) void Engine::endRendering() { inner_->endRendering(); - recorder_->add(Op::EndFrame, protocol::EndFrame{}); + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + recorder_->add(Op::EndFrame, protocol::EndFrame{static_cast(now.QuadPart)}); recorder_->flush(true); ++frames_; } @@ -250,7 +254,18 @@ void Engine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInterna 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::drawPointSprites(const imvu::PointSpriteVector& sprites) +{ + inner_->drawPointSprites(sprites); + if (sprites.first == nullptr || sprites.last <= sprites.first) { + return; + } + const auto count = static_cast(sprites.last - sprites.first); + if (std::byte* payload = recorder_->add(Op::DrawPointSprites, protocol::DrawPointSprites{count}, + std::size_t{count} * sizeof(imvu::PointSprite))) { + std::memcpy(payload, sprites.first, std::size_t{count} * sizeof(imvu::PointSprite)); + } +} void Engine::drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount, const std::uint16_t* indices, diff --git a/docs/architecture.md b/docs/architecture.md index 3c92d40..9159231 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -35,15 +35,23 @@ client exits. ## Status -A spike: the adapter still has IMVU's Direct3D 9 engine draw the scene (the pass-through), and the renderer process -only draws a box in the 3D view's corner, pulsing with the client's frames. Checked in the client (Direct3D 11 in -software, on a machine without a graphics driver): the box draws over the scene from the 64-bit process, keeps in -step with the client's frames, clicks on it reach the scene, and each renderer process exits with its engine (room -changes) and with the client. The client keeps two engines alive in a room, so two processes (about 15 MB each); -one process serving all of a client's engines would be leaner. +The renderer process draws the client's whole scene, over the right half of the 3D view, beside the client's own +Direct3D renderer on the left (the adapter still forwards everything to it): `T5DE_RENDERER_VIEW=full` or `off` +changes that. Checked in rooms: the halves match, particles included. -Next: the protocol carrying each frame's calls and the resources (buffers, textures), and the renderer process -drawing the scene itself, with IMVU's Direct3D engine left as the fallback. +- The command stream (`include/t5de/protocol/commands.hpp`) carries state, transforms, lights, fog, textures, + buffers (copied when created or updated) and draws, in chunks through the channel; the adapter waits for each to + be taken, and gives up on a renderer process that stops taking them (the client's renderer carries on). +- The renderer process reproduces IMVU's Direct3D 9 setup (`renderer/shaders/fixed_function.hlsl`), particles + included (point sprites, drawn as instanced squares), with a pipeline per combination of states. +- Speed: on the development VM (software rendering) a room frame takes 8 to 60 ms to draw; the time from the + client finishing a frame to ours being presented averages about 60 ms. The renderer process draws only the + newest frame when it falls behind, and keeps at most one frame queued. Over Remote Desktop, the half drawn by the + renderer process (another process's window, composited by Windows) visibly trails the client's half by more + than that; that delay comes after presenting, and is to be checked on a real graphics card and display. + +Next: render targets (offscreen frames are skipped), the renderer process drawing the whole view in place of the +client's renderer, and one renderer process for all of a client's views. ## Building diff --git a/docs/interface.md b/docs/interface.md index 8e61222..7dc01f8 100644 --- a/docs/interface.md +++ b/docs/interface.md @@ -71,7 +71,12 @@ virtual methods, IMVU's code only uses an engine through helpers that call its v vertical field of view, Direct3D's 0 to 1 depth) and an orthographic one for 2D, orthonormal view rotations, opaque clear colors, ambient light with zero alpha, linear fog (1 to 50), scrolling texture transforms. Furniture vertices are interleaved: positions and normals are 12-byte elements 32 bytes apart, so strides matter. -- Still unknown: point sprites (`drawPointSprites`, particles), render targets, `CompoundRegion` (texture update +- Particles (`drawPointSprites`): a Visual C++ 2010 `std::vector` of 16-byte points (position, color), drawn as + Direct3D 9 point sprites: squares viewport height / distance pixels across (Direct3D 9's defaults clamp them to + 1 to 64; IMVU sets only the size, 1, and the scale factors 0, 0, 1), textured over the whole square. +- Fog: linear table fog. Direct3D 9 fogs by eye distance only for projections whose (3, 4) element is +1; IMVU's are + right-handed (-1), so it fogs by depth buffer value (0 to 1) against start 1 and end 50: in effect no fog. +- Still unknown: render targets, `CompoundRegion` (texture update regions), `newRenderTexture`'s flags, and what `beginRendering` does with a null target. MSVC orders overloaded virtual methods unlike their declaration, so our declarations never overload virtual methods. - Memory crosses the boundary only through virtual methods (destructors, control blocks), so each side can use its diff --git a/include/t5de/imvu/types.hpp b/include/t5de/imvu/types.hpp index 2ae9687..94212d6 100644 --- a/include/t5de/imvu/types.hpp +++ b/include/t5de/imvu/types.hpp @@ -90,8 +90,21 @@ static_assert(sizeof(VertexAttributeArray) == 12); // (width, height, pitch, rowBytes, pixels, ...). class Pixmap; -// Unknown so far: used only by drawPointSprites (particles), as a Visual C++ 2010 std::vector. -struct PointSpriteVector; +// A particle (drawPointSprites): a point drawn as a camera-facing square, textured, about viewport height / distance +// pixels across (1 to 64), as the Direct3D engine's point sprites are. +struct PointSprite { + Vec3f position; + Color color; +}; + +static_assert(sizeof(PointSprite) == 16); + +// std::vector as Visual C++ 2010 lays it out (release builds): its elements are [first, last). +struct PointSpriteVector { + const PointSprite* first; + const PointSprite* last; + const PointSprite* end; // of the allocation +}; // Region lists IMVU passes to texture updates; unknown so far. class CompoundRegion; diff --git a/include/t5de/protocol/commands.hpp b/include/t5de/protocol/commands.hpp index 7a8c03b..808156e 100644 --- a/include/t5de/protocol/commands.hpp +++ b/include/t5de/protocol/commands.hpp @@ -32,6 +32,7 @@ enum class Op : std::uint16_t { DestroyBuffer, DrawTriangles, DrawArrays, + DrawPointSprites, }; struct Header { @@ -47,7 +48,7 @@ struct BeginFrame { }; struct EndFrame { - std::uint32_t unused; + std::uint64_t sentAt; // the adapter's QueryPerformanceCounter (the same clock in every process) }; struct Clear { @@ -172,6 +173,12 @@ struct DrawArrays { std::uint32_t hasColors; }; +// Particles: payload count sprites of 16 bytes, a position (3 floats) and a color. Each is a camera-facing square, +// viewport height / distance pixels across (1 to 64), textured with the first texture over the whole square. +struct DrawPointSprites { + std::uint32_t count; +}; + static_assert(sizeof(Header) == 8 && sizeof(SetProjection) == 72 && sizeof(SetLight) == 28 && sizeof(DrawArrays) == 20); diff --git a/renderer/CMakeLists.txt b/renderer/CMakeLists.txt index 84935b4..9b4ea0f 100644 --- a/renderer/CMakeLists.txt +++ b/renderer/CMakeLists.txt @@ -32,7 +32,7 @@ function(t5de_embed_shader file name) endfunction() t5de_embed_shader(fixed_function.hlsl fixedFunctionHlsl) -add_executable(t5de_renderer WIN32 main.cpp scene.cpp) +add_executable(t5de_renderer WIN32 log.cpp 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) diff --git a/renderer/log.cpp b/renderer/log.cpp new file mode 100644 index 0000000..15220e6 --- /dev/null +++ b/renderer/log.cpp @@ -0,0 +1,30 @@ +#include "log.hpp" + +#include + +#include +#include + +namespace t5de::log { + +// Each line is one appending write, so lines from several processes don't mix. +void write(std::string_view line) +{ + static const HANDLE file = [] { + wchar_t temp[MAX_PATH + 1]; + const DWORD length = GetTempPathW(MAX_PATH + 1, temp); + if (length == 0 || length > MAX_PATH) { + return INVALID_HANDLE_VALUE; + } + return CreateFileW((std::filesystem::path(temp) / L"t5de-renderer-process.log").c_str(), FILE_APPEND_DATA, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, nullptr); + }(); + if (file != INVALID_HANDLE_VALUE) { + const std::string text = std::format("{}: {}\n", GetCurrentProcessId(), line); + DWORD written = 0; + WriteFile(file, text.data(), static_cast(text.size()), &written, nullptr); + } +} + +} // namespace t5de::log diff --git a/renderer/log.hpp b/renderer/log.hpp new file mode 100644 index 0000000..a05c380 --- /dev/null +++ b/renderer/log.hpp @@ -0,0 +1,18 @@ +#pragma once +// The renderer processes' log: %TEMP%\t5de-renderer-process.log, shared by all of them (lines start with the pid). + +#include +#include +#include + +namespace t5de::log { + +void write(std::string_view line); + +template +void info(std::format_string format, Args&&... args) +{ + write(std::format(format, std::forward(args)...)); +} + +} // namespace t5de::log diff --git a/renderer/main.cpp b/renderer/main.cpp index e6f49e7..8660582 100644 --- a/renderer/main.cpp +++ b/renderer/main.cpp @@ -5,11 +5,11 @@ #include -#include +#include +#include #include #include -#include #include #include #include @@ -19,6 +19,8 @@ #include "EngineFactoryD3D11.h" #include "RefCntAutoPtr.hpp" +#include "RenderDeviceD3D11.h" +#include "log.hpp" #include "scene.hpp" #include "t5de/protocol/channel.hpp" @@ -28,34 +30,14 @@ using namespace Diligent; using t5de::protocol::Channel; using t5de::protocol::View; -// 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)...)); -} +using t5de::log::info; // 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 : "", + info("Diligent {}: {}{}{}", severities[severity < 4 ? severity : 3], function != nullptr ? function : "", function != nullptr ? ": " : "", message != nullptr ? message : ""); } @@ -140,7 +122,7 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) return 4; } - log("started for window {:#x}, view {}", channel->window, static_cast(channel->view)); + info("started for window {:#x}, view {}", channel->window, static_cast(channel->view)); IEngineFactoryD3D11* factory = GetEngineFactoryD3D11(); factory->SetMessageCallback(diligent_message); RefCntAutoPtr device; @@ -148,9 +130,17 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) EngineD3D11CreateInfo engineInfo; factory->CreateDeviceAndContextsD3D11(engineInfo, &device, &context); if (!device) { - log("can't create a Direct3D 11 device"); + info("can't create a Direct3D 11 device"); return 5; } + // Show each frame as soon as it's drawn: Direct3D 11 otherwise lets 3 frames queue, which the view would trail by. + if (RefCntAutoPtr d3d11{device, IID_RenderDeviceD3D11}) { + IDXGIDevice1* dxgi = nullptr; + if (SUCCEEDED(d3d11->GetD3D11Device()->QueryInterface(__uuidof(IDXGIDevice1), reinterpret_cast(&dxgi)))) { + dxgi->SetMaximumFrameLatency(1); + dxgi->Release(); + } + } RefCntAutoPtr swapChain; SwapChainDesc swapChainDesc; swapChainDesc.ColorBufferFormat = TEX_FORMAT_RGBA8_UNORM; // no sRGB conversion, like the client's renderer @@ -158,7 +148,7 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) factory->CreateSwapChainD3D11(device, context, swapChainDesc, FullScreenModeDesc{}, Win32NativeWindow{window}, &swapChain); if (!swapChain) { - log("can't create a swap chain"); + info("can't create a swap chain"); return 6; } t5de::Scene scene(device, context, swapChain); @@ -177,6 +167,7 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) LARGE_INTEGER frequency; QueryPerformanceFrequency(&frequency); double drawTime = 0; + double latency = 0; int drawn = 0; int skipped = 0; RECT placed{}; @@ -219,7 +210,7 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) 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); + info("drawing {}x{} at {} in the client's 3D view", width, height, place.left); } LARGE_INTEGER start, end; QueryPerformanceCounter(&start); @@ -227,10 +218,14 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) swapChain->Present(0); QueryPerformanceCounter(&end); drawTime += static_cast(end.QuadPart - start.QuadPart) / static_cast(frequency.QuadPart); + latency += static_cast(end.QuadPart - static_cast(scene.lastSentAt())) / + 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); + info("last 300 frames: {:.1f} ms each to draw and present, {:.1f} ms from the client's end of frame to " + "presented, {} skipped to catch up", + drawTime * 1000 / drawn, latency * 1000 / drawn, skipped); drawTime = 0; + latency = 0; drawn = 0; skipped = 0; } @@ -238,7 +233,7 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) frame.erase(frame.begin(), frame.begin() + static_cast(complete)); } - log("exiting"); + info("exiting"); DestroyWindow(window); return 0; } diff --git a/renderer/scene.cpp b/renderer/scene.cpp index fc5b54b..b06d94c 100644 --- a/renderer/scene.cpp +++ b/renderer/scene.cpp @@ -4,6 +4,7 @@ #include #include "fixed_function_hlsl.hpp" +#include "log.hpp" namespace t5de { @@ -104,6 +105,9 @@ bool Scene::execute(std::span commands, int offsetX, bool draw) } break; case Op::EndFrame: + if (const auto end = command->body()) { + lastSentAt_ = end->sentAt; + } drew = drew || onscreen_; onscreen_ = false; break; @@ -200,6 +204,11 @@ bool Scene::execute(std::span commands, int offsetX, bool draw) drawArrays(*command); } break; + case Op::DrawPointSprites: + if (onscreen_) { + drawPointSprites(*command); + } + break; } } return drew; @@ -208,6 +217,8 @@ bool Scene::execute(std::span commands, int offsetX, bool draw) void Scene::setProjection(const protocol::SetProjection& projection) { std::memcpy(constants_.projection, projection.m, sizeof(constants_.projection)); + constants_.viewport[0] = static_cast(projection.width); + constants_.viewport[1] = static_cast(projection.height); if (!onscreen_) { return; } @@ -452,34 +463,12 @@ void Scene::draw(IBuffer* indices, std::uint32_t indexCount, IBuffer* positions, 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) { + if (prepare(streams, false) == 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}) { @@ -496,9 +485,80 @@ void Scene::draw(IBuffer* indices, std::uint32_t indexCount, IBuffer* positions, context_->DrawIndexed(attributes); } -const Scene::Shaders& Scene::shaders(std::uint32_t streams, std::uint32_t textures) +Scene::Pipeline* Scene::prepare(std::uint32_t streams, bool sprites) { - const std::uint32_t key = streams | (textures << 3); + // 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; + Pipeline* selected = pipeline(streams, textures, sprites); + if (selected == nullptr) { + return nullptr; + } + + void* mapped = nullptr; + context_->MapBuffer(constantBuffer_, MAP_WRITE, MAP_FLAG_DISCARD, mapped); + if (mapped == nullptr) { + return nullptr; + } + 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); + return selected; +} + +void Scene::drawPointSprites(const protocol::Command& command) +{ + const auto sprites = command.body(); + const auto payload = command.payload(); + if (!sprites || sprites->count == 0 || payload.size() < std::size_t{sprites->count} * 16) { + return; + } + BufferDesc desc; + desc.Name = "Client particles"; + desc.Size = std::size_t{sprites->count} * 16; + desc.BindFlags = BIND_VERTEX_BUFFER; + desc.Usage = USAGE_IMMUTABLE; + BufferData contents; + contents.pData = payload.data(); + contents.DataSize = desc.Size; + RefCntAutoPtr buffer; + device_->CreateBuffer(desc, &contents, &buffer); + if (!buffer || prepare(0, true) == nullptr) { + return; + } + if (spritesLogged_ < 5) { + ++spritesLogged_; + const Texture* texture = textures_.contains(state_.texture0) ? &textures_[state_.texture0] : nullptr; + log::info("particles: {}, texture {} {}x{} flags {}, blending {} mode {}, depth test {} write {}, alpha test {} " + "reference {}, lighting {}, fog {}, ambient ({:.2f} {:.2f} {:.2f}), texture transform [{} {} {} {} {} {}]", + sprites->count, state_.texture0, texture ? texture->width : 0, texture ? texture->height : 0, + texture ? texture->flags : 0, state_.blending, state_.blendMode, state_.depthTest, state_.depthWrite, + constants_.flags[1], constants_.flags[2], constants_.flags[0], constants_.fog[2], constants_.ambient[0], + constants_.ambient[1], constants_.ambient[2], constants_.texTransform0[0], constants_.texTransform0[1], + constants_.texTransform0[2], constants_.texTransform0[4], constants_.texTransform0[5], + constants_.texTransform0[6]); + } + IBuffer* buffers[] = {buffer}; + context_->SetVertexBuffers(0, 1, buffers, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION, + SET_VERTEX_BUFFERS_FLAG_RESET); + DrawAttribs attributes; + attributes.NumVertices = 4; + attributes.NumInstances = sprites->count; + context_->Draw(attributes); +} + +const Scene::Shaders& Scene::shaders(std::uint32_t streams, std::uint32_t textures, bool sprites) +{ + const std::uint32_t key = streams | (textures << 3) | (sprites ? 1u << 5 : 0); if (const auto found = shaders_.find(key); found != shaders_.end()) { return found->second; } @@ -512,8 +572,8 @@ const Scene::Shaders& Scene::shaders(std::uint32_t streams, std::uint32_t textur 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"; + info.Desc.Name = sprites ? "Fixed function particles (vertex)" : "Fixed function (vertex)"; + info.EntryPoint = sprites ? "vs_sprite" : "vs_main"; device_->CreateShader(info, &created.vertex); info.Desc.ShaderType = SHADER_TYPE_PIXEL; info.Desc.Name = "Fixed function (pixel)"; @@ -522,17 +582,18 @@ const Scene::Shaders& Scene::shaders(std::uint32_t streams, std::uint32_t textur return shaders_[key] = std::move(created); } -Scene::Pipeline* Scene::pipeline(std::uint32_t streams, std::uint32_t textures) +Scene::Pipeline* Scene::pipeline(std::uint32_t streams, std::uint32_t textures, bool sprites) { const std::uint32_t blend = state_.blending ? state_.blendMode : 0; + const bool culling = state_.culling && !sprites; // point sprites aren't culled 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); + (state_.depthWrite ? 1u << 9 : 0) | (culling ? 1u << 10 : 0) | + (state_.normalInverting ? 1u << 11 : 0) | (sprites ? 1u << 12 : 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); + const Shaders& compiled = shaders(streams, textures, sprites); if (!compiled.vertex || !compiled.pixel) { return nullptr; } @@ -545,9 +606,9 @@ Scene::Pipeline* Scene::pipeline(std::uint32_t streams, std::uint32_t textures) graphics.NumRenderTargets = 1; graphics.RTVFormats[0] = window.ColorBufferFormat; graphics.DSVFormat = window.DepthBufferFormat; - graphics.PrimitiveTopology = PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + graphics.PrimitiveTopology = sprites ? PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP : 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.CullMode = culling ? CULL_MODE_BACK : CULL_MODE_NONE; graphics.RasterizerDesc.FrontCounterClockwise = !state_.normalInverting; graphics.DepthStencilDesc.DepthEnable = state_.depthTest; graphics.DepthStencilDesc.DepthWriteEnable = state_.depthWrite; @@ -564,17 +625,30 @@ Scene::Pipeline* Scene::pipeline(std::uint32_t streams, std::uint32_t textures) LayoutElement elements[4]; std::uint32_t count = 0; - elements[count] = LayoutElement{0, count, 3, VT_FLOAT32, False}; - ++count; - if ((streams & hasNormals) != 0) { + if (sprites) { + // One buffer of sprites, each a position and a color, read once per square (instance). + elements[0] = LayoutElement{0, 0, 3, VT_FLOAT32, False}; + elements[1] = LayoutElement{3, 0, 4, VT_UINT8, True}; + elements[1].RelativeOffset = 12; + for (LayoutElement& element : std::span(elements).first(2)) { + element.Stride = 16; + element.Frequency = INPUT_ELEMENT_FREQUENCY_PER_INSTANCE; + element.InstanceDataStepRate = 1; + } + count = 2; + } else { + elements[count] = LayoutElement{0, count, 3, VT_FLOAT32, False}; + ++count; + } + if (!sprites && (streams & hasNormals) != 0) { elements[count] = LayoutElement{1, count, 3, VT_FLOAT32, False}; ++count; } - if ((streams & hasTexCoords) != 0) { + if (!sprites && (streams & hasTexCoords) != 0) { elements[count] = LayoutElement{2, count, 2, VT_FLOAT32, False}; ++count; } - if ((streams & hasColors) != 0) { + if (!sprites && (streams & hasColors) != 0) { elements[count] = LayoutElement{3, count, 4, VT_UINT8, True}; ++count; } diff --git a/renderer/scene.hpp b/renderer/scene.hpp index 092970c..ba1555b 100644 --- a/renderer/scene.hpp +++ b/renderer/scene.hpp @@ -23,6 +23,9 @@ public: // present). bool execute(std::span commands, int offsetX, bool draw); + // When the adapter sent the last frame carried out (QueryPerformanceCounter). + [[nodiscard]] std::uint64_t lastSentAt() const { return lastSentAt_; } + private: struct Texture { Diligent::RefCntAutoPtr texture; @@ -63,6 +66,7 @@ private: float fogColor[4]; float fog[4]; float flags[4]; + float viewport[4]; }; // What a draw needs from the state, as set by the client. @@ -85,12 +89,16 @@ private: void setState(const protocol::SetState& state); void drawTriangles(const protocol::DrawTriangles& draw); void drawArrays(const protocol::Command& command); + void drawPointSprites(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); + // The pipeline for the current state, with these vertex streams and textures, for triangles or particles. + Pipeline* pipeline(std::uint32_t streams, std::uint32_t textures, bool sprites); + const Shaders& shaders(std::uint32_t streams, std::uint32_t textures, bool sprites); + // Picks the pipeline for the current state and sets it up for a draw: constants, textures. Null if it can't. + Pipeline* prepare(std::uint32_t streams, bool sprites); Diligent::ITextureView* textureView(std::uint32_t id); Diligent::RefCntAutoPtr device_; @@ -108,6 +116,8 @@ private: State state_; bool onscreen_ = false; // in a frame drawn into the window bool draw_ = true; + std::uint64_t lastSentAt_ = 0; + int spritesLogged_ = 0; int offsetX_ = 0; }; diff --git a/renderer/shaders/fixed_function.hlsl b/renderer/shaders/fixed_function.hlsl index 8808e57..931f593 100644 --- a/renderer/shaders/fixed_function.hlsl +++ b/renderer/shaders/fixed_function.hlsl @@ -3,7 +3,13 @@ // 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. +// - an alpha test passing pixels whose alpha is greater than the reference, then linear fog. Direct3D 9 fogs by +// distance from the eye only for projections with +1 as their (3, 4) element; IMVU's (right-handed) have -1, so +// it fogs by the depth buffer's value (0 to 1) against the fog's start and end (in scene units, 1 to 50 in +// rooms): in effect, no fog. Reproduced as is. +// +// Particles (vs_sprite) are Direct3D 9 point sprites there: camera-facing squares, viewport height / distance pixels +// across (1 to 64), textured over the whole square; here each is drawn as an instance of a 4-vertex strip. // // Compiled for each combination of the vertex streams present (HAS_NORMALS, HAS_TEX_COORDS, HAS_COLORS) and the // number of textures (TEXTURES). @@ -21,6 +27,7 @@ cbuffer Constants 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) + float4 g_Viewport; // xy: size in pixels }; struct VSInput @@ -43,7 +50,6 @@ struct PSInput float4 Color : COLOR0; float2 TexCoord0 : TEXCOORD0; float2 TexCoord1 : TEXCOORD1; - float Depth : TEXCOORD2; }; void vs_main(in VSInput input, out PSInput output) @@ -51,7 +57,6 @@ 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 @@ -85,6 +90,29 @@ void vs_main(in VSInput input, out PSInput output) output.TexCoord1 = float2(dot(uv, g_TexTransform1[0].xyz), dot(uv, g_TexTransform1[1].xyz)); } +struct SpriteInput +{ + float3 Position : ATTRIB0; + float4 Color : ATTRIB3; // bytes B, G, R, A + uint Corner : SV_VertexID; +}; + +void vs_sprite(in SpriteInput input, out PSInput output) +{ + float4 view = mul(mul(float4(input.Position, 1.0), g_World), g_View); + float4 clip = mul(view, g_Projection); + float size = clamp(g_Viewport.y / max(length(view.xyz), 1e-4), 1.0, 64.0); + float2 corner = float2((input.Corner & 1) != 0 ? 1.0 : -1.0, (input.Corner & 2) != 0 ? -1.0 : 1.0); + clip.xy += corner * size / g_Viewport.xy * clip.w; // half the size each way, in clip space + output.Position = clip; + + // No normals: lit by the ambient light alone. + float4 material = input.Color.zyxw; + output.Color = g_Flags.x > 0.5 ? float4(saturate(g_Ambient.rgb * material.rgb), material.a) : material; + output.TexCoord0 = float2(corner.x * 0.5 + 0.5, 0.5 - corner.y * 0.5); + output.TexCoord1 = output.TexCoord0; +} + #if TEXTURES >= 1 Texture2D g_Texture0; SamplerState g_Texture0_sampler; @@ -110,7 +138,7 @@ float4 ps_main(in PSInput input) : SV_Target } if (g_Fog.z > 0.5) { - float visible = saturate((g_Fog.y - input.Depth) / max(g_Fog.y - g_Fog.x, 1e-6)); + float visible = saturate((g_Fog.y - input.Position.z) / max(g_Fog.y - g_Fog.x, 1e-6)); color.rgb = lerp(g_FogColor.rgb, color.rgb, visible); } return color;