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;