Index: ps/trunk/source/renderer/OverlayRenderer.cpp =================================================================== --- ps/trunk/source/renderer/OverlayRenderer.cpp (revision 26834) +++ ps/trunk/source/renderer/OverlayRenderer.cpp (revision 26835) @@ -1,780 +1,781 @@ /* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "OverlayRenderer.h" #include "graphics/Camera.h" #include "graphics/LOSTexture.h" #include "graphics/Overlay.h" #include "graphics/ShaderManager.h" #include "graphics/Terrain.h" #include "graphics/TextureManager.h" #include "lib/hash.h" #include "lib/ogl.h" #include "maths/MathUtil.h" #include "maths/Quaternion.h" #include "ps/CStrInternStatic.h" #include "ps/Game.h" #include "ps/Profile.h" #include "renderer/DebugRenderer.h" #include "renderer/Renderer.h" #include "renderer/SceneRenderer.h" #include "renderer/TexturedLineRData.h" #include "renderer/VertexArray.h" #include "renderer/VertexBuffer.h" #include "renderer/VertexBufferManager.h" #include "simulation2/components/ICmpWaterManager.h" #include "simulation2/Simulation2.h" #include "simulation2/system/SimContext.h" #include namespace { CShaderTechniquePtr GetOverlayLineShaderTechnique(const CShaderDefines& defines) { return g_Renderer.GetShaderManager().LoadEffect(str_overlay_line, defines); } } // anonymous namespace /** * Key used to group quads into batches for more efficient rendering. Currently groups by the combination * of the main texture and the texture mask, to minimize texture swapping during rendering. */ struct QuadBatchKey { QuadBatchKey (const CTexturePtr& texture, const CTexturePtr& textureMask) : m_Texture(texture), m_TextureMask(textureMask) { } bool operator==(const QuadBatchKey& other) const { return (m_Texture == other.m_Texture && m_TextureMask == other.m_TextureMask); } CTexturePtr m_Texture; CTexturePtr m_TextureMask; }; struct QuadBatchHash { std::size_t operator()(const QuadBatchKey& d) const { size_t seed = 0; hash_combine(seed, d.m_Texture); hash_combine(seed, d.m_TextureMask); return seed; } }; /** * Holds information about a single quad rendering batch. */ class QuadBatchData : public CRenderData { public: QuadBatchData() : m_IndicesBase(0), m_NumRenderQuads(0) { } /// Holds the quad overlay structures requested to be rendered in this batch. Must be cleared /// after each frame. std::vector m_Quads; /// Start index of this batch into the dedicated quad indices VertexArray (see OverlayInternals). size_t m_IndicesBase; /// Amount of quads to actually render in this batch. Potentially (although unlikely to be) /// different from m_Quads.size() due to restrictions on the total amount of quads that can be /// rendered. Must be reset after each frame. size_t m_NumRenderQuads; }; struct OverlayRendererInternals { using QuadBatchMap = std::unordered_map; OverlayRendererInternals(); ~OverlayRendererInternals(){ } std::vector lines; std::vector texlines; std::vector sprites; std::vector quads; std::vector spheres; QuadBatchMap quadBatchMap; // Dedicated vertex/index buffers for rendering all quads (to within the limits set by // MAX_QUAD_OVERLAYS). VertexArray quadVertices; VertexArray::Attribute quadAttributePos; VertexArray::Attribute quadAttributeColor; VertexArray::Attribute quadAttributeUV; VertexIndexArray quadIndices; - /// Maximum amount of quad overlays we support for rendering. This limit is set to be able to - /// render all quads from a single dedicated VB without having to reallocate it, which is much - /// faster in the typical case of rendering only a handful of quads. When modifying this value, - /// you must take care for the new amount of quads to fit in a single VBO (which is not likely - /// to be a problem). + // Maximum amount of quad overlays we support for rendering. This limit is set to be able to + // render all quads from a single dedicated VB without having to reallocate it, which is much + // faster in the typical case of rendering only a handful of quads. When modifying this value, + // you must take care for the new amount of quads to fit in a single backend buffer (which is + // not likely to be a problem). static const size_t MAX_QUAD_OVERLAYS = 1024; // Sets of commonly-(re)used shader defines. CShaderDefines defsOverlayLineNormal; CShaderDefines defsOverlayLineAlwaysVisible; CShaderDefines defsQuadOverlay; // Geometry for a unit sphere std::vector sphereVertexes; std::vector sphereIndexes; void GenerateSphere(); - /// Performs one-time setup. Called from CRenderer::Open, after graphics capabilities have - /// been detected. Note that no VBOs must be created before this is called, since the shader - /// path and graphics capabilities are not guaranteed to be stable before this point. + // Performs one-time setup. Called from CRenderer::Open, after graphics capabilities have + // been detected. Note that no backend buffer must be created before this is called, since + // the shader path and graphics capabilities are not guaranteed to be stable before this + // point. void Initialize(); }; const float OverlayRenderer::OVERLAY_VOFFSET = 0.2f; OverlayRendererInternals::OverlayRendererInternals() : quadVertices(Renderer::Backend::GL::CBuffer::Type::VERTEX, true), quadIndices(false) { quadAttributePos.format = Renderer::Backend::Format::R32G32B32_SFLOAT; quadVertices.AddAttribute(&quadAttributePos); quadAttributeColor.format = Renderer::Backend::Format::R8G8B8A8_UNORM; quadVertices.AddAttribute(&quadAttributeColor); quadAttributeUV.format = Renderer::Backend::Format::R16G16_SINT; quadVertices.AddAttribute(&quadAttributeUV); // Note that we're reusing the textured overlay line shader for the quad overlay rendering. This // is because their code is almost identical; the only difference is that for the quad overlays // we want to use a vertex color stream as opposed to an objectColor uniform. To this end, the // shader has been set up to switch between the two behaviours based on the USE_OBJECTCOLOR define. defsOverlayLineNormal.Add(str_USE_OBJECTCOLOR, str_1); defsOverlayLineAlwaysVisible.Add(str_USE_OBJECTCOLOR, str_1); defsOverlayLineAlwaysVisible.Add(str_IGNORE_LOS, str_1); } void OverlayRendererInternals::Initialize() { // Perform any initialization after graphics capabilities have been detected. Notably, - // only at this point can we safely allocate VBOs (in contrast to e.g. in the constructor), + // only at this point can we safely allocate backend buffer (in contrast to e.g. in the constructor), // because their creation depends on the shader path, which is not reliably set before this point. quadVertices.SetNumberOfVertices(MAX_QUAD_OVERLAYS * 4); quadVertices.Layout(); // allocate backing store quadIndices.SetNumberOfVertices(MAX_QUAD_OVERLAYS * 6); quadIndices.Layout(); // allocate backing store // Since the quads in the vertex array are independent and always consist of exactly 4 vertices per quad, the // indices are always the same; we can therefore fill in all the indices once and pretty much forget about // them. We then also no longer need its backing store, since we never change any indices afterwards. VertexArrayIterator index = quadIndices.GetIterator(); for (u16 i = 0; i < static_cast(MAX_QUAD_OVERLAYS); ++i) { *index++ = i * 4 + 0; *index++ = i * 4 + 1; *index++ = i * 4 + 2; *index++ = i * 4 + 2; *index++ = i * 4 + 3; *index++ = i * 4 + 0; } quadIndices.Upload(); quadIndices.FreeBackingStore(); } OverlayRenderer::OverlayRenderer() { m = new OverlayRendererInternals(); } OverlayRenderer::~OverlayRenderer() { delete m; } void OverlayRenderer::Initialize() { m->Initialize(); } void OverlayRenderer::Submit(SOverlayLine* line) { m->lines.push_back(line); } void OverlayRenderer::Submit(SOverlayTexturedLine* line) { // Simplify the rest of the code by guaranteeing non-empty lines if (line->m_Coords.empty()) return; m->texlines.push_back(line); } void OverlayRenderer::Submit(SOverlaySprite* overlay) { m->sprites.push_back(overlay); } void OverlayRenderer::Submit(SOverlayQuad* overlay) { m->quads.push_back(overlay); } void OverlayRenderer::Submit(SOverlaySphere* overlay) { m->spheres.push_back(overlay); } void OverlayRenderer::EndFrame() { m->lines.clear(); m->texlines.clear(); m->sprites.clear(); m->quads.clear(); m->spheres.clear(); // this should leave the capacity unchanged, which is okay since it // won't be very large or very variable // Empty the batch rendering data structures, but keep their key mappings around for the next frames for (OverlayRendererInternals::QuadBatchMap::iterator it = m->quadBatchMap.begin(); it != m->quadBatchMap.end(); ++it) { QuadBatchData& quadBatchData = (it->second); quadBatchData.m_Quads.clear(); quadBatchData.m_NumRenderQuads = 0; quadBatchData.m_IndicesBase = 0; } } void OverlayRenderer::PrepareForRendering() { PROFILE3("prepare overlays"); // This is where we should do something like sort the overlays by // color/sprite/etc for more efficient rendering for (size_t i = 0; i < m->texlines.size(); ++i) { SOverlayTexturedLine* line = m->texlines[i]; if (!line->m_RenderData) { line->m_RenderData = std::make_shared(); line->m_RenderData->Update(*line); // We assume the overlay line will get replaced by the caller // if terrain changes, so we don't need to detect that here and // call Update again. Also we assume the caller won't change // any of the parameters after first submitting the line. } } // Group quad overlays by their texture/mask combination for efficient rendering // TODO: consider doing this directly in Submit() for (size_t i = 0; i < m->quads.size(); ++i) { SOverlayQuad* const quad = m->quads[i]; QuadBatchKey textures(quad->m_Texture, quad->m_TextureMask); QuadBatchData& batchRenderData = m->quadBatchMap[textures]; // will create entry if it doesn't already exist // add overlay to list of quads batchRenderData.m_Quads.push_back(quad); } const CVector3D vOffset(0, OverlayRenderer::OVERLAY_VOFFSET, 0); // Write quad overlay vertices/indices to VA backing store VertexArrayIterator vertexPos = m->quadAttributePos.GetIterator(); VertexArrayIterator vertexColor = m->quadAttributeColor.GetIterator(); VertexArrayIterator vertexUV = m->quadAttributeUV.GetIterator(); size_t indicesIdx = 0; size_t totalNumQuads = 0; for (OverlayRendererInternals::QuadBatchMap::iterator it = m->quadBatchMap.begin(); it != m->quadBatchMap.end(); ++it) { QuadBatchData& batchRenderData = (it->second); batchRenderData.m_NumRenderQuads = 0; if (batchRenderData.m_Quads.empty()) continue; // Remember the current index into the (entire) indices array as our base offset for this batch batchRenderData.m_IndicesBase = indicesIdx; // points to the index where each iteration's vertices will be appended for (size_t i = 0; i < batchRenderData.m_Quads.size() && totalNumQuads < OverlayRendererInternals::MAX_QUAD_OVERLAYS; i++) { const SOverlayQuad* quad = batchRenderData.m_Quads[i]; const SColor4ub quadColor = quad->m_Color.AsSColor4ub(); *vertexPos++ = quad->m_Corners[0] + vOffset; *vertexPos++ = quad->m_Corners[1] + vOffset; *vertexPos++ = quad->m_Corners[2] + vOffset; *vertexPos++ = quad->m_Corners[3] + vOffset; (*vertexUV)[0] = 0; (*vertexUV)[1] = 0; ++vertexUV; (*vertexUV)[0] = 0; (*vertexUV)[1] = 1; ++vertexUV; (*vertexUV)[0] = 1; (*vertexUV)[1] = 1; ++vertexUV; (*vertexUV)[0] = 1; (*vertexUV)[1] = 0; ++vertexUV; *vertexColor++ = quadColor; *vertexColor++ = quadColor; *vertexColor++ = quadColor; *vertexColor++ = quadColor; indicesIdx += 6; totalNumQuads++; batchRenderData.m_NumRenderQuads++; } } m->quadVertices.Upload(); // don't free the backing store! we'll overwrite it on the next frame to save a reallocation. m->quadVertices.PrepareForRendering(); } void OverlayRenderer::RenderOverlaysBeforeWater( Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext) { PROFILE3_GPU("overlays (before)"); GPU_SCOPED_LABEL(deviceCommandContext, "Render overlays before water"); for (SOverlayLine* line : m->lines) { if (line->m_Coords.empty()) continue; g_Renderer.GetDebugRenderer().DrawLine(line->m_Coords, line->m_Color, static_cast(line->m_Thickness)); } } void OverlayRenderer::RenderOverlaysAfterWater( Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext) { PROFILE3_GPU("overlays (after)"); GPU_SCOPED_LABEL(deviceCommandContext, "Render overlays after water"); RenderTexturedOverlayLines(deviceCommandContext); RenderQuadOverlays(deviceCommandContext); RenderSphereOverlays(deviceCommandContext); } void OverlayRenderer::RenderTexturedOverlayLines(Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext) { if (m->texlines.empty()) return; CLOSTexture& los = g_Renderer.GetSceneRenderer().GetScene().GetLOSTexture(); // ---------------------------------------------------------------------------------------- CShaderTechniquePtr shaderTechTexLineNormal = GetOverlayLineShaderTechnique(m->defsOverlayLineNormal); if (shaderTechTexLineNormal) { Renderer::Backend::GraphicsPipelineStateDesc pipelineStateDesc = shaderTechTexLineNormal->GetGraphicsPipelineStateDesc(); pipelineStateDesc.depthStencilState.depthWriteEnabled = false; pipelineStateDesc.blendState.enabled = true; pipelineStateDesc.blendState.srcColorBlendFactor = pipelineStateDesc.blendState.srcAlphaBlendFactor = Renderer::Backend::BlendFactor::SRC_ALPHA; pipelineStateDesc.blendState.dstColorBlendFactor = pipelineStateDesc.blendState.dstAlphaBlendFactor = Renderer::Backend::BlendFactor::ONE_MINUS_SRC_ALPHA; pipelineStateDesc.blendState.colorBlendOp = pipelineStateDesc.blendState.alphaBlendOp = Renderer::Backend::BlendOp::ADD; if (g_Renderer.GetSceneRenderer().GetOverlayRenderMode() == WIREFRAME) pipelineStateDesc.rasterizationState.polygonMode = Renderer::Backend::PolygonMode::LINE; deviceCommandContext->SetGraphicsPipelineState(pipelineStateDesc); deviceCommandContext->BeginPass(); Renderer::Backend::GL::CShaderProgram* shaderTexLineNormal = shaderTechTexLineNormal->GetShader(); shaderTexLineNormal->BindTexture(str_losTex, los.GetTexture()); shaderTexLineNormal->Uniform(str_losTransform, los.GetTextureMatrix()[0], los.GetTextureMatrix()[12], 0.f, 0.f); shaderTexLineNormal->Uniform(str_transform, g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection()); // batch render only the non-always-visible overlay lines using the normal shader RenderTexturedOverlayLines(deviceCommandContext, shaderTexLineNormal, false); deviceCommandContext->EndPass(); } // ---------------------------------------------------------------------------------------- CShaderTechniquePtr shaderTechTexLineAlwaysVisible = GetOverlayLineShaderTechnique(m->defsOverlayLineAlwaysVisible); if (shaderTechTexLineAlwaysVisible) { Renderer::Backend::GraphicsPipelineStateDesc pipelineStateDesc = shaderTechTexLineAlwaysVisible->GetGraphicsPipelineStateDesc(); pipelineStateDesc.depthStencilState.depthWriteEnabled = false; pipelineStateDesc.blendState.enabled = true; pipelineStateDesc.blendState.srcColorBlendFactor = pipelineStateDesc.blendState.srcAlphaBlendFactor = Renderer::Backend::BlendFactor::SRC_ALPHA; pipelineStateDesc.blendState.dstColorBlendFactor = pipelineStateDesc.blendState.dstAlphaBlendFactor = Renderer::Backend::BlendFactor::ONE_MINUS_SRC_ALPHA; pipelineStateDesc.blendState.colorBlendOp = pipelineStateDesc.blendState.alphaBlendOp = Renderer::Backend::BlendOp::ADD; if (g_Renderer.GetSceneRenderer().GetOverlayRenderMode() == WIREFRAME) pipelineStateDesc.rasterizationState.polygonMode = Renderer::Backend::PolygonMode::LINE; deviceCommandContext->SetGraphicsPipelineState(pipelineStateDesc); deviceCommandContext->BeginPass(); Renderer::Backend::GL::CShaderProgram* shaderTexLineAlwaysVisible = shaderTechTexLineAlwaysVisible->GetShader(); // TODO: losTex and losTransform are unused in the always visible shader; see if these can be safely omitted shaderTexLineAlwaysVisible->BindTexture(str_losTex, los.GetTexture()); shaderTexLineAlwaysVisible->Uniform(str_losTransform, los.GetTextureMatrix()[0], los.GetTextureMatrix()[12], 0.f, 0.f); shaderTexLineAlwaysVisible->Uniform(str_transform, g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection()); // batch render only the always-visible overlay lines using the LoS-ignored shader RenderTexturedOverlayLines(deviceCommandContext, shaderTexLineAlwaysVisible, true); deviceCommandContext->EndPass(); } // ---------------------------------------------------------------------------------------- // TODO: the shaders should probably be responsible for unbinding their textures deviceCommandContext->BindTexture(1, GL_TEXTURE_2D, 0); deviceCommandContext->BindTexture(0, GL_TEXTURE_2D, 0); } void OverlayRenderer::RenderTexturedOverlayLines( Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext, Renderer::Backend::GL::CShaderProgram* shader, bool alwaysVisible) { for (size_t i = 0; i < m->texlines.size(); ++i) { SOverlayTexturedLine* line = m->texlines[i]; // render only those lines matching the requested alwaysVisible status if (!line->m_RenderData || line->m_AlwaysVisible != alwaysVisible) continue; ENSURE(line->m_RenderData); line->m_RenderData->Render(deviceCommandContext, *line, shader); } } void OverlayRenderer::RenderQuadOverlays( Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext) { if (m->quadBatchMap.empty()) return; CShaderTechniquePtr shaderTech = GetOverlayLineShaderTechnique(m->defsQuadOverlay); if (!shaderTech) return; Renderer::Backend::GraphicsPipelineStateDesc pipelineStateDesc = shaderTech->GetGraphicsPipelineStateDesc(); pipelineStateDesc.depthStencilState.depthWriteEnabled = false; pipelineStateDesc.blendState.enabled = true; pipelineStateDesc.blendState.srcColorBlendFactor = pipelineStateDesc.blendState.srcAlphaBlendFactor = Renderer::Backend::BlendFactor::SRC_ALPHA; pipelineStateDesc.blendState.dstColorBlendFactor = pipelineStateDesc.blendState.dstAlphaBlendFactor = Renderer::Backend::BlendFactor::ONE_MINUS_SRC_ALPHA; pipelineStateDesc.blendState.colorBlendOp = pipelineStateDesc.blendState.alphaBlendOp = Renderer::Backend::BlendOp::ADD; if (g_Renderer.GetSceneRenderer().GetOverlayRenderMode() == WIREFRAME) pipelineStateDesc.rasterizationState.polygonMode = Renderer::Backend::PolygonMode::LINE; deviceCommandContext->SetGraphicsPipelineState(pipelineStateDesc); deviceCommandContext->BeginPass(); Renderer::Backend::GL::CShaderProgram* shader = shaderTech->GetShader(); CLOSTexture& los = g_Renderer.GetSceneRenderer().GetScene().GetLOSTexture(); shader->BindTexture(str_losTex, los.GetTexture()); shader->Uniform(str_losTransform, los.GetTextureMatrix()[0], los.GetTextureMatrix()[12], 0.f, 0.f); shader->Uniform(str_transform, g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection()); m->quadVertices.UploadIfNeeded(deviceCommandContext); m->quadIndices.UploadIfNeeded(deviceCommandContext); const uint32_t vertexStride = m->quadVertices.GetStride(); const uint32_t firstVertexOffset = m->quadVertices.GetOffset() * vertexStride; for (OverlayRendererInternals::QuadBatchMap::iterator it = m->quadBatchMap.begin(); it != m->quadBatchMap.end(); ++it) { QuadBatchData& batchRenderData = it->second; const size_t batchNumQuads = batchRenderData.m_NumRenderQuads; if (batchNumQuads == 0) continue; const QuadBatchKey& maskPair = it->first; maskPair.m_Texture->UploadBackendTextureIfNeeded(deviceCommandContext); maskPair.m_TextureMask->UploadBackendTextureIfNeeded(deviceCommandContext); shader->BindTexture(str_baseTex, maskPair.m_Texture->GetBackendTexture()); shader->BindTexture(str_maskTex, maskPair.m_TextureMask->GetBackendTexture()); // TODO: move setting format out of the loop, we might want move the offset // to the index offset when it's supported. deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::POSITION, m->quadAttributePos.format, firstVertexOffset + m->quadAttributePos.offset, vertexStride, 0); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::COLOR, m->quadAttributeColor.format, firstVertexOffset + m->quadAttributeColor.offset, vertexStride, 0); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::UV0, m->quadAttributeUV.format, firstVertexOffset + m->quadAttributeUV.offset, vertexStride, 0); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::UV1, m->quadAttributeUV.format, firstVertexOffset + m->quadAttributeUV.offset, vertexStride, 0); deviceCommandContext->SetVertexBuffer(0, m->quadVertices.GetBuffer()); deviceCommandContext->SetIndexBuffer(m->quadIndices.GetBuffer()); deviceCommandContext->DrawIndexed(m->quadIndices.GetOffset() + batchRenderData.m_IndicesBase, batchNumQuads * 6, 0); g_Renderer.GetStats().m_DrawCalls++; g_Renderer.GetStats().m_OverlayTris += batchNumQuads*2; } deviceCommandContext->EndPass(); // TODO: the shader should probably be responsible for unbinding its textures deviceCommandContext->BindTexture(1, GL_TEXTURE_2D, 0); deviceCommandContext->BindTexture(0, GL_TEXTURE_2D, 0); } void OverlayRenderer::RenderForegroundOverlays( Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext, const CCamera& viewCamera) { PROFILE3_GPU("overlays (fg)"); const CVector3D right = -viewCamera.GetOrientation().GetLeft(); const CVector3D up = viewCamera.GetOrientation().GetUp(); CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_foreground_overlay); Renderer::Backend::GraphicsPipelineStateDesc pipelineStateDesc = tech->GetGraphicsPipelineStateDesc(); pipelineStateDesc.depthStencilState.depthTestEnabled = false; pipelineStateDesc.blendState.enabled = true; pipelineStateDesc.blendState.srcColorBlendFactor = pipelineStateDesc.blendState.srcAlphaBlendFactor = Renderer::Backend::BlendFactor::SRC_ALPHA; pipelineStateDesc.blendState.dstColorBlendFactor = pipelineStateDesc.blendState.dstAlphaBlendFactor = Renderer::Backend::BlendFactor::ONE_MINUS_SRC_ALPHA; pipelineStateDesc.blendState.colorBlendOp = pipelineStateDesc.blendState.alphaBlendOp = Renderer::Backend::BlendOp::ADD; if (g_Renderer.GetSceneRenderer().GetOverlayRenderMode() == WIREFRAME) pipelineStateDesc.rasterizationState.polygonMode = Renderer::Backend::PolygonMode::LINE; deviceCommandContext->SetGraphicsPipelineState(pipelineStateDesc); deviceCommandContext->BeginPass(); Renderer::Backend::GL::CShaderProgram* shader = tech->GetShader(); shader->Uniform(str_transform, g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection()); const CVector2D uvs[6] = { {0.0f, 1.0f}, {1.0f, 1.0f}, {1.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 0.0f}, {0.0f, 0.0f}, }; deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, 0, 0, 0); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::UV0, Renderer::Backend::Format::R32G32_SFLOAT, 0, 0, 1); deviceCommandContext->SetVertexBufferData(1, &uvs[0]); for (size_t i = 0; i < m->sprites.size(); ++i) { SOverlaySprite* sprite = m->sprites[i]; if (!i || sprite->m_Texture != m->sprites[i - 1]->m_Texture) { sprite->m_Texture->UploadBackendTextureIfNeeded(deviceCommandContext); shader->BindTexture(str_baseTex, sprite->m_Texture->GetBackendTexture()); } shader->Uniform(str_colorMul, sprite->m_Color); const CVector3D position[6] = { sprite->m_Position + right*sprite->m_X0 + up*sprite->m_Y0, sprite->m_Position + right*sprite->m_X1 + up*sprite->m_Y0, sprite->m_Position + right*sprite->m_X1 + up*sprite->m_Y1, sprite->m_Position + right*sprite->m_X0 + up*sprite->m_Y0, sprite->m_Position + right*sprite->m_X1 + up*sprite->m_Y1, sprite->m_Position + right*sprite->m_X0 + up*sprite->m_Y1 }; deviceCommandContext->SetVertexBufferData(0, &position[0].X); deviceCommandContext->Draw(0, 6); g_Renderer.GetStats().m_DrawCalls++; g_Renderer.GetStats().m_OverlayTris += 2; } deviceCommandContext->EndPass(); } static void TessellateSphereFace(const CVector3D& a, u16 ai, const CVector3D& b, u16 bi, const CVector3D& c, u16 ci, std::vector& vertexes, std::vector& indexes, int level) { if (level == 0) { indexes.push_back(ai); indexes.push_back(bi); indexes.push_back(ci); } else { CVector3D d = (a + b).Normalized(); CVector3D e = (b + c).Normalized(); CVector3D f = (c + a).Normalized(); int di = vertexes.size() / 3; vertexes.push_back(d.X); vertexes.push_back(d.Y); vertexes.push_back(d.Z); int ei = vertexes.size() / 3; vertexes.push_back(e.X); vertexes.push_back(e.Y); vertexes.push_back(e.Z); int fi = vertexes.size() / 3; vertexes.push_back(f.X); vertexes.push_back(f.Y); vertexes.push_back(f.Z); TessellateSphereFace(a,ai, d,di, f,fi, vertexes, indexes, level-1); TessellateSphereFace(d,di, b,bi, e,ei, vertexes, indexes, level-1); TessellateSphereFace(f,fi, e,ei, c,ci, vertexes, indexes, level-1); TessellateSphereFace(d,di, e,ei, f,fi, vertexes, indexes, level-1); } } static void TessellateSphere(std::vector& vertexes, std::vector& indexes, int level) { /* Start with a tetrahedron, then tessellate */ float s = sqrtf(0.5f); #define VERT(a,b,c) vertexes.push_back(a); vertexes.push_back(b); vertexes.push_back(c); VERT(-s, 0, -s); VERT( s, 0, -s); VERT( s, 0, s); VERT(-s, 0, s); VERT( 0, -1, 0); VERT( 0, 1, 0); #define FACE(a,b,c) \ TessellateSphereFace( \ CVector3D(vertexes[a*3], vertexes[a*3+1], vertexes[a*3+2]), a, \ CVector3D(vertexes[b*3], vertexes[b*3+1], vertexes[b*3+2]), b, \ CVector3D(vertexes[c*3], vertexes[c*3+1], vertexes[c*3+2]), c, \ vertexes, indexes, level); FACE(0,4,1); FACE(1,4,2); FACE(2,4,3); FACE(3,4,0); FACE(1,5,0); FACE(2,5,1); FACE(3,5,2); FACE(0,5,3); #undef FACE #undef VERT } void OverlayRendererInternals::GenerateSphere() { if (sphereVertexes.empty()) TessellateSphere(sphereVertexes, sphereIndexes, 3); } void OverlayRenderer::RenderSphereOverlays( Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext) { PROFILE3_GPU("overlays (spheres)"); if (m->spheres.empty()) return; Renderer::Backend::GL::CShaderProgram* shader; CShaderTechniquePtr tech; tech = g_Renderer.GetShaderManager().LoadEffect(str_overlay_solid); Renderer::Backend::GraphicsPipelineStateDesc pipelineStateDesc = tech->GetGraphicsPipelineStateDesc(); pipelineStateDesc.depthStencilState.depthWriteEnabled = false; pipelineStateDesc.blendState.enabled = true; pipelineStateDesc.blendState.srcColorBlendFactor = pipelineStateDesc.blendState.srcAlphaBlendFactor = Renderer::Backend::BlendFactor::SRC_ALPHA; pipelineStateDesc.blendState.dstColorBlendFactor = pipelineStateDesc.blendState.dstAlphaBlendFactor = Renderer::Backend::BlendFactor::ONE_MINUS_SRC_ALPHA; pipelineStateDesc.blendState.colorBlendOp = pipelineStateDesc.blendState.alphaBlendOp = Renderer::Backend::BlendOp::ADD; deviceCommandContext->SetGraphicsPipelineState(pipelineStateDesc); deviceCommandContext->BeginPass(); shader = tech->GetShader(); shader->Uniform(str_transform, g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection()); m->GenerateSphere(); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, 0, 0, 0); deviceCommandContext->SetVertexBufferData(0, m->sphereVertexes.data()); deviceCommandContext->SetIndexBufferData(m->sphereIndexes.data()); for (size_t i = 0; i < m->spheres.size(); ++i) { SOverlaySphere* sphere = m->spheres[i]; CMatrix3D transform; transform.SetIdentity(); transform.Scale(sphere->m_Radius, sphere->m_Radius, sphere->m_Radius); transform.Translate(sphere->m_Center); shader->Uniform(str_instancingTransform, transform); shader->Uniform(str_color, sphere->m_Color); deviceCommandContext->DrawIndexed(0, m->sphereIndexes.size(), 0); g_Renderer.GetStats().m_DrawCalls++; g_Renderer.GetStats().m_OverlayTris = m->sphereIndexes.size()/3; } deviceCommandContext->EndPass(); } Index: ps/trunk/source/renderer/OverlayRenderer.h =================================================================== --- ps/trunk/source/renderer/OverlayRenderer.h (revision 26834) +++ ps/trunk/source/renderer/OverlayRenderer.h (revision 26835) @@ -1,159 +1,159 @@ /* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_OVERLAYRENDERER #define INCLUDED_OVERLAYRENDERER #include "graphics/ShaderProgram.h" #include "renderer/backend/gl/DeviceCommandContext.h" struct SOverlayLine; struct SOverlayTexturedLine; struct SOverlaySprite; struct SOverlayQuad; struct SOverlaySphere; class CCamera; struct OverlayRendererInternals; /** * Class OverlayRenderer: Render various bits of data that overlay the * game world (selection circles, health bars, etc). */ class OverlayRenderer { NONCOPYABLE(OverlayRenderer); public: OverlayRenderer(); ~OverlayRenderer(); /** * Performs one-time initialization. Called by CRenderer::Open after graphics - * capabilities and the shader path have been determined (notably VBO support). + * capabilities and the shader path have been determined. */ void Initialize(); /** * Add a line overlay for rendering in this frame. * @param overlay Must be non-null. The pointed-to object must remain valid at least * until the end of the frame. */ void Submit(SOverlayLine* overlay); /** * Add a textured line overlay for rendering in this frame. * @param overlay Must be non-null. The pointed-to object must remain valid at least * until the end of the frame. */ void Submit(SOverlayTexturedLine* overlay); /** * Add a sprite overlay for rendering in this frame. * @param overlay Must be non-null. The pointed-to object must remain valid at least * until the end of the frame. */ void Submit(SOverlaySprite* overlay); /** * Add a textured quad overlay for rendering in this frame. * @param overlay Must be non-null. The pointed-to object must remain valid at least * until the end of the frame. */ void Submit(SOverlayQuad* overlay); /** * Add a sphere overlay for rendering in this frame. * @param overlay Must be non-null. The pointed-to object must remain valid at least * until the end of the frame. */ void Submit(SOverlaySphere* overlay); /** * Prepare internal data structures for rendering. * Must be called after all Submit calls for a frame, and before * any rendering calls. */ void PrepareForRendering(); /** * Reset the list of submitted overlays. */ void EndFrame(); /** * Render all the submitted overlays that are embedded in the world * (i.e. rendered behind other objects in the normal 3D way) * and should be drawn before water (i.e. may be visible under the water) */ void RenderOverlaysBeforeWater( Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext); /** * Render all the submitted overlays that are embedded in the world * (i.e. rendered behind other objects in the normal 3D way) * and should be drawn after water (i.e. may be visible on top of the water) */ void RenderOverlaysAfterWater( Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext); /** * Render all the submitted overlays that should appear on top of everything * in the world. * @param viewCamera camera to be used for billboard computations */ void RenderForegroundOverlays( Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext, const CCamera& viewCamera); /// Small vertical offset of overlays from terrain to prevent visual glitches static const float OVERLAY_VOFFSET; private: /** * Helper method; renders all overlay lines currently registered in the internals. Batch- * renders textured overlay lines batched according to their visibility status by delegating * to RenderTexturedOverlayLines(CShaderProgramPtr, bool). */ void RenderTexturedOverlayLines(Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext); /** * Helper method; renders those overlay lines currently registered in the internals (i.e. * in m->texlines) for which the 'always visible' flag equals @p alwaysVisible. Used for * batch rendering the overlay lines according to their alwaysVisible status, as this * requires a separate shader to be used. */ void RenderTexturedOverlayLines( Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext, Renderer::Backend::GL::CShaderProgram* shader, bool alwaysVisible); /** * Helper method; batch-renders all registered quad overlays, batched by their texture for effiency. */ void RenderQuadOverlays(Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext); /** * Helper method; batch-renders all sphere quad overlays. */ void RenderSphereOverlays(Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext); private: OverlayRendererInternals* m; }; #endif // INCLUDED_OVERLAYRENDERER Index: ps/trunk/source/renderer/TexturedLineRData.cpp =================================================================== --- ps/trunk/source/renderer/TexturedLineRData.cpp (revision 26834) +++ ps/trunk/source/renderer/TexturedLineRData.cpp (revision 26835) @@ -1,460 +1,462 @@ /* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "TexturedLineRData.h" #include "graphics/ShaderProgram.h" #include "graphics/Terrain.h" #include "maths/Frustum.h" #include "maths/MathUtil.h" #include "maths/Quaternion.h" #include "ps/CStrInternStatic.h" #include "renderer/OverlayRenderer.h" #include "renderer/Renderer.h" #include "simulation2/Simulation2.h" #include "simulation2/system/SimContext.h" #include "simulation2/components/ICmpWaterManager.h" /* Note: this implementation uses g_VBMan directly rather than access it through the nicer VertexArray interface, * because it allows you to work with variable amounts of vertices and indices more easily. New code should prefer * to use VertexArray where possible, though. */ void CTexturedLineRData::Render( Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext, const SOverlayTexturedLine& line, Renderer::Backend::GL::CShaderProgram* shader) { if (!m_VB || !m_VBIndices) return; // might have failed to allocate // -- render main line quad strip ---------------------- line.m_TextureBase->UploadBackendTextureIfNeeded(deviceCommandContext); line.m_TextureMask->UploadBackendTextureIfNeeded(deviceCommandContext); m_VB->m_Owner->UploadIfNeeded(deviceCommandContext); m_VBIndices->m_Owner->UploadIfNeeded(deviceCommandContext); shader->BindTexture(str_baseTex, line.m_TextureBase->GetBackendTexture()); shader->BindTexture(str_maskTex, line.m_TextureMask->GetBackendTexture()); shader->Uniform(str_objectColor, line.m_Color); const uint32_t stride = sizeof(CTexturedLineRData::SVertex); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(CTexturedLineRData::SVertex, m_Position), stride, 0); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::UV0, Renderer::Backend::Format::R32G32_SFLOAT, offsetof(CTexturedLineRData::SVertex, m_UVs), stride, 0); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::UV1, Renderer::Backend::Format::R32G32_SFLOAT, offsetof(CTexturedLineRData::SVertex, m_UVs), stride, 0); deviceCommandContext->SetVertexBuffer(0, m_VB->m_Owner->GetBuffer()); deviceCommandContext->SetIndexBuffer(m_VBIndices->m_Owner->GetBuffer()); deviceCommandContext->DrawIndexed(m_VBIndices->m_Index, m_VBIndices->m_Count, 0); g_Renderer.GetStats().m_DrawCalls++; g_Renderer.GetStats().m_OverlayTris += m_VBIndices->m_Count/3; } void CTexturedLineRData::Update(const SOverlayTexturedLine& line) { m_VBIndices.Reset(); m_VB.Reset(); if (!line.m_SimContext) { debug_warn(L"[TexturedLineRData] No SimContext set for textured overlay line, cannot render (no terrain data)"); return; } float v = 0.f; std::vector vertices; std::vector indices; const size_t n = line.m_Coords.size(); // number of line points bool closed = line.m_Closed; ENSURE(n >= 2); // minimum needed to avoid errors (also minimum value to make sense, can't draw a line between 1 point) // In each iteration, p1 is the position of vertex i, p0 is i-1, p2 is i+1. // To avoid slightly expensive terrain computations we cycle these around and // recompute p2 at the end of each iteration. CVector3D p0; CVector3D p1(line.m_Coords[0].X, 0, line.m_Coords[0].Y); CVector3D p2(line.m_Coords[1].X, 0, line.m_Coords[1].Y); if (closed) // grab the ending point so as to close the loop p0 = CVector3D(line.m_Coords[n - 1].X, 0, line.m_Coords[n - 1].Y); else // we don't want to loop around and use the direction towards the other end of the line, so create an artificial p0 that // extends the p2 -> p1 direction, and use that point instead p0 = p1 + (p1 - p2); bool p1floating = false; bool p2floating = false; // Compute terrain heights, clamped to the water height (and remember whether // each point was floating on water, for normal computation later) // TODO: if we ever support more than one water level per map, recompute this per point CmpPtr cmpWaterManager(*line.m_SimContext, SYSTEM_ENTITY); float w = cmpWaterManager ? cmpWaterManager->GetExactWaterLevel(p0.X, p0.Z) : 0.f; const CTerrain& terrain = line.m_SimContext->GetTerrain(); p0.Y = terrain.GetExactGroundLevel(p0.X, p0.Z); if (p0.Y < w) p0.Y = w; p1.Y = terrain.GetExactGroundLevel(p1.X, p1.Z); if (p1.Y < w) { p1.Y = w; p1floating = true; } p2.Y = terrain.GetExactGroundLevel(p2.X, p2.Z); if (p2.Y < w) { p2.Y = w; p2floating = true; } for (size_t i = 0; i < n; ++i) { // For vertex i, compute bisector of lines (i-1)..(i) and (i)..(i+1) // perpendicular to terrain normal // Normal is vertical if on water, else computed from terrain CVector3D norm; if (p1floating) norm = CVector3D(0, 1, 0); else norm = terrain.CalcExactNormal(p1.X, p1.Z); CVector3D b = ((p1 - p0).Normalized() + (p2 - p1).Normalized()).Cross(norm); // Adjust bisector length to match the line thickness, along the line's width float l = b.Dot((p2 - p1).Normalized().Cross(norm)); if (fabs(l) > 0.000001f) // avoid unlikely divide-by-zero b *= line.m_Thickness / l; // Push vertices and indices for each quad in GL_TRIANGLES order. The two triangles of each quad are indexed using // the winding orders (BR, BL, TR) and (TR, BL, TL) (where BR is bottom-right of this iteration's quad, TR top-right etc). SVertex vertex1(p1 + b + norm*OverlayRenderer::OVERLAY_VOFFSET, 0.f, v); SVertex vertex2(p1 - b + norm*OverlayRenderer::OVERLAY_VOFFSET, 1.f, v); vertices.push_back(vertex1); vertices.push_back(vertex2); u16 vertexCount = static_cast(vertices.size()); u16 index1 = vertexCount - 2; // index of vertex1 in this iteration (TR of this quad) u16 index2 = vertexCount - 1; // index of the vertex2 in this iteration (TL of this quad) if (i == 0) { // initial two vertices to continue building triangles from (n must be >= 2 for this to work) indices.push_back(index1); indices.push_back(index2); } else { u16 index1Prev = vertexCount - 4; // index of the vertex1 in the previous iteration (BR of this quad) u16 index2Prev = vertexCount - 3; // index of the vertex2 in the previous iteration (BL of this quad) ENSURE(index1Prev < vertexCount); ENSURE(index2Prev < vertexCount); // Add two corner points from last iteration and join with one of our own corners to create triangle 1 // (don't need to do this if i == 1 because i == 0 are the first two ones, they don't need to be copied) if (i > 1) { indices.push_back(index1Prev); indices.push_back(index2Prev); } indices.push_back(index1); // complete triangle 1 // create triangle 2, specifying the adjacent side's vertices in the opposite order from triangle 1 indices.push_back(index1); indices.push_back(index2Prev); indices.push_back(index2); } // alternate V coordinate for debugging v = 1 - v; // cycle the p's and compute the new p2 p0 = p1; p1 = p2; p1floating = p2floating; // if in closed mode, wrap around the coordinate array for p2 -- otherwise, extend linearly if (!closed && i == n-2) // next iteration is the last point of the line, so create an artificial p2 that extends the p0 -> p1 direction p2 = p1 + (p1 - p0); else p2 = CVector3D(line.m_Coords[(i + 2) % n].X, 0, line.m_Coords[(i + 2) % n].Y); p2.Y = terrain.GetExactGroundLevel(p2.X, p2.Z); if (p2.Y < w) { p2.Y = w; p2floating = true; } else p2floating = false; } if (closed) { // close the path if (n % 2 == 0) { u16 vertexCount = static_cast(vertices.size()); indices.push_back(vertexCount - 2); indices.push_back(vertexCount - 1); indices.push_back(0); indices.push_back(0); indices.push_back(vertexCount - 1); indices.push_back(1); } else { // add two vertices to have the good UVs for the last quad SVertex vertex1(vertices[0].m_Position, 0.f, 1.f); SVertex vertex2(vertices[1].m_Position, 1.f, 1.f); vertices.push_back(vertex1); vertices.push_back(vertex2); u16 vertexCount = static_cast(vertices.size()); indices.push_back(vertexCount - 4); indices.push_back(vertexCount - 3); indices.push_back(vertexCount - 2); indices.push_back(vertexCount - 2); indices.push_back(vertexCount - 3); indices.push_back(vertexCount - 1); } } else { // Create start and end caps. On either end, this is done by taking the centroid between the last and second-to-last pair of // vertices that was generated along the path (i.e. the vertex1's and vertex2's from above), taking a directional vector // between them, and drawing the line cap in the plane given by the two butt-end corner points plus said vector. std::vector capIndices; std::vector capVertices; // create end cap CreateLineCap( line, // the order of these vertices is important here, swapping them produces caps at the wrong side vertices[vertices.size()-2].m_Position, // top-right vertex of last quad vertices[vertices.size()-1].m_Position, // top-left vertex of last quad // directional vector between centroids of last vertex pair and second-to-last vertex pair (Centroid(vertices[vertices.size()-2], vertices[vertices.size()-1]) - Centroid(vertices[vertices.size()-4], vertices[vertices.size()-3])).Normalized(), line.m_EndCapType, capVertices, capIndices ); for (unsigned i = 0; i < capIndices.size(); i++) capIndices[i] += static_cast(vertices.size()); vertices.insert(vertices.end(), capVertices.begin(), capVertices.end()); indices.insert(indices.end(), capIndices.begin(), capIndices.end()); capIndices.clear(); capVertices.clear(); // create start cap CreateLineCap( line, // the order of these vertices is important here, swapping them produces caps at the wrong side vertices[1].m_Position, vertices[0].m_Position, // directional vector between centroids of first vertex pair and second vertex pair (Centroid(vertices[1], vertices[0]) - Centroid(vertices[3], vertices[2])).Normalized(), line.m_StartCapType, capVertices, capIndices ); for (unsigned i = 0; i < capIndices.size(); i++) capIndices[i] += static_cast(vertices.size()); vertices.insert(vertices.end(), capVertices.begin(), capVertices.end()); indices.insert(indices.end(), capIndices.begin(), capIndices.end()); } if (vertices.empty() || indices.empty()) return; // Indices for triangles, so must be multiple of 3. ENSURE(indices.size() % 3 == 0); m_BoundingBox = CBoundingBoxAligned(); for (const SVertex& vertex : vertices) m_BoundingBox += vertex.m_Position; m_VB = g_VBMan.AllocateChunk( sizeof(SVertex), vertices.size(), Renderer::Backend::GL::CBuffer::Type::VERTEX, false); - if (m_VB) // allocation might fail (e.g. due to too many vertices) + // Allocation might fail (e.g. due to too many vertices). + if (m_VB) { - m_VB->m_Owner->UpdateChunkVertices(m_VB.Get(), &vertices[0]); // copy data into VBO + // Copy data into backend buffer. + m_VB->m_Owner->UpdateChunkVertices(m_VB.Get(), &vertices[0]); for (size_t k = 0; k < indices.size(); ++k) indices[k] += static_cast(m_VB->m_Index); m_VBIndices = g_VBMan.AllocateChunk( sizeof(u16), indices.size(), Renderer::Backend::GL::CBuffer::Type::INDEX, false); if (m_VBIndices) m_VBIndices->m_Owner->UpdateChunkVertices(m_VBIndices.Get(), &indices[0]); } } void CTexturedLineRData::CreateLineCap(const SOverlayTexturedLine& line, const CVector3D& corner1, const CVector3D& corner2, const CVector3D& lineDirectionNormal, SOverlayTexturedLine::LineCapType endCapType, std::vector& verticesOut, std::vector& indicesOut) { if (endCapType == SOverlayTexturedLine::LINECAP_FLAT) return; // no action needed, this is the default // When not in closed mode, we've created artificial points for the start- and endpoints that extend the line in the // direction of the first and the last segment, respectively. Thus, we know both the start and endpoints have perpendicular // butt endings, i.e. the end corner vertices on either side of the line extend perpendicularly from the segment direction. // That is to say, when viewed from the top, we will have something like // . // this: and not like this: /| // ----+ / | // | / . // | / // ----+ / // int roundCapPoints = 8; // amount of points to sample along the semicircle for rounded caps (including corner points) float radius = line.m_Thickness; CVector3D centerPoint = (corner1 + corner2) * 0.5f; SVertex centerVertex(centerPoint, 0.5f, 0.5f); u16 indexOffset = static_cast(verticesOut.size()); // index offset in verticesOut from where we start adding our vertices switch (endCapType) { case SOverlayTexturedLine::LINECAP_SHARP: { roundCapPoints = 3; // creates only one point directly ahead radius *= 1.5f; // make it a bit sharper (note that we don't use the radius for the butt-end corner points so it should be ok) centerVertex.m_UVs[0] = 0.480f; // slight visual correction to make the texture match up better at the corner points } FALLTHROUGH; case SOverlayTexturedLine::LINECAP_ROUND: { // Draw a rounded line cap in the 3D plane of the line specified by the two corner points and the normal vector of the // line's direction. The terrain normal at the centroid between the two corner points is perpendicular to this plane. // The way this works is by taking a vector from the corner points' centroid to one of the corner points (which is then // of radius length), and rotate it around the terrain normal vector in that centroid. This will rotate the vector in // the line's plane, producing the desired rounded cap. // To please OpenGL's winding order, this angle needs to be negated depending on whether we start rotating from // the (center -> corner1) or (center -> corner2) vector. For the (center -> corner2) vector, we apparently need to use // the negated angle. float stepAngle = -(float)(M_PI/(roundCapPoints-1)); // Push the vertices in triangle fan order (easy to generate GL_TRIANGLES indices for afterwards) // Note that we're manually adding the corner vertices instead of having them be generated by the rotating vector. // This is because we want to support an overly large radius to make the sharp line ending look sharper. verticesOut.push_back(centerVertex); verticesOut.push_back(SVertex(corner2, 0.f, 0.f)); // Get the base vector that we will incrementally rotate in the cap plane to produce the radial sample points. // Normally corner2 - centerPoint would suffice for this since it is of radius length, but we want to support custom // radii to support tuning the 'sharpness' of sharp end caps (see above) CVector3D rotationBaseVector = (corner2 - centerPoint).Normalized() * radius; // Calculate the normal vector of the plane in which we're going to be drawing the line cap. This is the vector that // is perpendicular to both baseVector and the 'lineDirectionNormal' vector indicating the direction of the line. // Note that we shouldn't use terrain->CalcExactNormal() here because if the line is being rendered on top of water, // then CalcExactNormal will return the normal vector of the terrain that's underwater (which can be quite funky). CVector3D capPlaneNormal = lineDirectionNormal.Cross(rotationBaseVector).Normalized(); for (int i = 1; i < roundCapPoints - 1; ++i) { // Rotate the centerPoint -> corner vector by i*stepAngle radians around the cap plane normal at the center point. CQuaternion quatRotation; quatRotation.FromAxisAngle(capPlaneNormal, i * stepAngle); CVector3D worldPos3D = centerPoint + quatRotation.Rotate(rotationBaseVector); // Let v range from 0 to 1 as we move along the semi-circle, keep u fixed at 0 (i.e. curve the left vertical edge // of the texture around the edge of the semicircle) float u = 0.f; float v = Clamp((i / static_cast(roundCapPoints - 1)), 0.f, 1.f); // pos, u, v verticesOut.push_back(SVertex(worldPos3D, u, v)); } // connect back to the other butt-end corner point to complete the semicircle verticesOut.push_back(SVertex(corner1, 0.f, 1.f)); // now push indices in GL_TRIANGLES order; vertices[indexOffset] is the center vertex, vertices[indexOffset + 1] is the // first corner point, then a bunch of radial samples, and then at the end we have the other corner point again. So: for (int i=1; i < roundCapPoints; ++i) { indicesOut.push_back(indexOffset); // center vertex indicesOut.push_back(indexOffset + i); indicesOut.push_back(indexOffset + i + 1); } } break; case SOverlayTexturedLine::LINECAP_SQUARE: { // Extend the (corner1 -> corner2) vector along the direction normal and draw a square line ending consisting of // three triangles (sort of like a triangle fan) // NOTE: The order in which the vertices are pushed out determines the visibility, as they // are rendered only one-sided; the wrong order of vertices will make the cap visible only from the bottom. verticesOut.push_back(centerVertex); verticesOut.push_back(SVertex(corner2, 0.f, 0.f)); verticesOut.push_back(SVertex(corner2 + (lineDirectionNormal * (line.m_Thickness)), 0.f, 0.33333f)); // extend butt corner point 2 along the normal vector verticesOut.push_back(SVertex(corner1 + (lineDirectionNormal * (line.m_Thickness)), 0.f, 0.66666f)); // extend butt corner point 1 along the normal vector verticesOut.push_back(SVertex(corner1, 0.f, 1.0f)); // push butt corner point 1 for (int i=1; i < 4; ++i) { indicesOut.push_back(indexOffset); // center point indicesOut.push_back(indexOffset + i); indicesOut.push_back(indexOffset + i + 1); } } break; default: break; } } bool CTexturedLineRData::IsVisibleInFrustum(const CFrustum& frustum) const { return frustum.IsBoxVisible(m_BoundingBox); } Index: ps/trunk/source/renderer/VertexArray.cpp =================================================================== --- ps/trunk/source/renderer/VertexArray.cpp (revision 26834) +++ ps/trunk/source/renderer/VertexArray.cpp (revision 26835) @@ -1,311 +1,311 @@ /* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "lib/alignment.h" #include "lib/sysdep/rtl.h" #include "maths/Vector3D.h" #include "maths/Vector4D.h" #include "ps/CLogger.h" #include "graphics/Color.h" #include "graphics/SColor.h" #include "renderer/VertexArray.h" #include "renderer/VertexBuffer.h" #include "renderer/VertexBufferManager.h" namespace { size_t GetAttributeSize(const Renderer::Backend::Format format) { switch (format) { case Renderer::Backend::Format::R8G8B8A8_UNORM: FALLTHROUGH; case Renderer::Backend::Format::R8G8B8A8_UINT: return sizeof(u8) * 4; case Renderer::Backend::Format::A8_UNORM: return sizeof(u8); case Renderer::Backend::Format::R16_UNORM: FALLTHROUGH; case Renderer::Backend::Format::R16_UINT: FALLTHROUGH; case Renderer::Backend::Format::R16_SINT: return sizeof(u16); case Renderer::Backend::Format::R16G16_UNORM: FALLTHROUGH; case Renderer::Backend::Format::R16G16_UINT: FALLTHROUGH; case Renderer::Backend::Format::R16G16_SINT: return sizeof(u16) * 2; case Renderer::Backend::Format::R32_SFLOAT: return sizeof(float); case Renderer::Backend::Format::R32G32_SFLOAT: return sizeof(float) * 2; case Renderer::Backend::Format::R32G32B32_SFLOAT: return sizeof(float) * 3; case Renderer::Backend::Format::R32G32B32A32_SFLOAT: return sizeof(float) * 4; default: break; }; return 0; } } // anonymous namespace VertexArray::VertexArray( const Renderer::Backend::GL::CBuffer::Type type, const bool dynamic) : m_Type(type), m_Dynamic(dynamic) { m_NumberOfVertices = 0; m_BackingStore = 0; m_Stride = 0; } VertexArray::~VertexArray() { Free(); } // Free all resources on destruction or when a layout parameter changes void VertexArray::Free() { rtl_FreeAligned(m_BackingStore); m_BackingStore = 0; m_VB.Reset(); } // Set the number of vertices stored in the array void VertexArray::SetNumberOfVertices(const size_t numberOfVertices) { if (numberOfVertices == m_NumberOfVertices) return; Free(); m_NumberOfVertices = numberOfVertices; } // Add vertex attributes like Position, Normal, UV void VertexArray::AddAttribute(Attribute* attr) { // Attribute is supported is its size is greater than zero. ENSURE(GetAttributeSize(attr->format) > 0 && "Unsupported attribute."); attr->vertexArray = this; m_Attributes.push_back(attr); Free(); } // Template specialization for GetIterator(). // We can put this into the source file because only a fixed set of types // is supported for type safety. template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE( format == Renderer::Backend::Format::R32G32B32_SFLOAT || format == Renderer::Backend::Format::R32G32B32A32_SFLOAT); return vertexArray->MakeIterator(this); } template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE(format == Renderer::Backend::Format::R32G32B32A32_SFLOAT); return vertexArray->MakeIterator(this); } template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE(format == Renderer::Backend::Format::R32G32_SFLOAT); return vertexArray->MakeIterator(this); } template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE( format == Renderer::Backend::Format::R8G8B8A8_UNORM || format == Renderer::Backend::Format::R8G8B8A8_UINT); return vertexArray->MakeIterator(this); } template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE(format == Renderer::Backend::Format::R16_UINT); return vertexArray->MakeIterator(this); } template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE(format == Renderer::Backend::Format::R16G16_UINT); return vertexArray->MakeIterator(this); } template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE(format == Renderer::Backend::Format::A8_UNORM); return vertexArray->MakeIterator(this); } template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE( format == Renderer::Backend::Format::R8G8B8A8_UNORM || format == Renderer::Backend::Format::R8G8B8A8_UINT); return vertexArray->MakeIterator(this); } template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE(format == Renderer::Backend::Format::R16_SINT); return vertexArray->MakeIterator(this); } template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE(format == Renderer::Backend::Format::R16G16_SINT); return vertexArray->MakeIterator(this); } static size_t RoundStride(size_t stride) { if (stride <= 0) return 0; if (stride <= 4) return 4; if (stride <= 8) return 8; if (stride <= 16) return 16; return Align<32>(stride); } // Re-layout by assigning offsets on a first-come first-serve basis, // then round up to a reasonable stride. -// Backing store is also created here, VBOs are created on upload. +// Backing store is also created here, backend buffers are created on upload. void VertexArray::Layout() { Free(); m_Stride = 0; for (ssize_t idx = m_Attributes.size()-1; idx >= 0; --idx) { Attribute* attr = m_Attributes[idx]; if (attr->format == Renderer::Backend::Format::UNDEFINED) continue; const size_t attrSize = GetAttributeSize(attr->format); ENSURE(attrSize > 0); attr->offset = m_Stride; m_Stride += attrSize; if (m_Type == Renderer::Backend::GL::CBuffer::Type::VERTEX) m_Stride = Align<4>(m_Stride); } if (m_Type == Renderer::Backend::GL::CBuffer::Type::VERTEX) m_Stride = RoundStride(m_Stride); if (m_Stride) m_BackingStore = (char*)rtl_AllocateAligned(m_Stride * m_NumberOfVertices, 16); } void VertexArray::PrepareForRendering() { m_VB->m_Owner->PrepareForRendering(m_VB.Get()); } // (Re-)Upload the attributes. -// Create the VBO if necessary. +// Create the backend buffer if necessary. void VertexArray::Upload() { ENSURE(m_BackingStore); if (!m_VB) { m_VB = g_VBMan.AllocateChunk( m_Stride, m_NumberOfVertices, m_Type, m_Dynamic, m_BackingStore); } if (!m_VB) { - LOGERROR("Failed to allocate VBO for vertex array"); + LOGERROR("Failed to allocate backend buffer for vertex array"); return; } m_VB->m_Owner->UpdateChunkVertices(m_VB.Get(), m_BackingStore); } void VertexArray::UploadIfNeeded( Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext) { m_VB->m_Owner->UploadIfNeeded(deviceCommandContext); } // Free the backing store to save some memory void VertexArray::FreeBackingStore() { // In streaming modes, the backing store must be retained ENSURE(!CVertexBuffer::UseStreaming(m_Dynamic)); rtl_FreeAligned(m_BackingStore); m_BackingStore = 0; } VertexIndexArray::VertexIndexArray(const bool dynamic) : VertexArray(Renderer::Backend::GL::CBuffer::Type::INDEX, dynamic) { m_Attr.format = Renderer::Backend::Format::R16_UINT; AddAttribute(&m_Attr); } VertexArrayIterator VertexIndexArray::GetIterator() const { return m_Attr.GetIterator(); } Index: ps/trunk/source/renderer/VertexBuffer.cpp =================================================================== --- ps/trunk/source/renderer/VertexBuffer.cpp (revision 26834) +++ ps/trunk/source/renderer/VertexBuffer.cpp (revision 26835) @@ -1,325 +1,325 @@ /* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "VertexBuffer.h" #include "lib/ogl.h" #include "lib/sysdep/cpu.h" #include "ps/CLogger.h" #include "ps/Errors.h" #include "ps/VideoMode.h" #include "renderer/backend/gl/Device.h" #include "renderer/Renderer.h" #include #include #include // Absolute maximum (bytewise) size of each GL vertex buffer object. // Make it large enough for the maximum feasible mesh size (64K vertexes, // 64 bytes per vertex in InstancingModelRenderer). // TODO: measure what influence this has on performance constexpr std::size_t MAX_VB_SIZE_BYTES = 4 * 1024 * 1024; CVertexBuffer::CVertexBuffer( const char* name, const size_t vertexSize, const Renderer::Backend::GL::CBuffer::Type type, const bool dynamic) : CVertexBuffer(name, vertexSize, type, dynamic, MAX_VB_SIZE_BYTES) { } CVertexBuffer::CVertexBuffer( const char* name, const size_t vertexSize, const Renderer::Backend::GL::CBuffer::Type type, const bool dynamic, const size_t maximumBufferSize) : m_VertexSize(vertexSize), m_HasNeededChunks(false) { size_t size = maximumBufferSize; if (type == Renderer::Backend::GL::CBuffer::Type::VERTEX) { // We want to store 16-bit indices to any vertex in a buffer, so the // buffer must never be bigger than vertexSize*64K bytes since we can // address at most 64K of them with 16-bit indices size = std::min(size, vertexSize * 65536); } else if (type == Renderer::Backend::GL::CBuffer::Type::INDEX) { ENSURE(vertexSize == sizeof(u16)); } // store max/free vertex counts m_MaxVertices = m_FreeVertices = size / vertexSize; m_Buffer = g_VideoMode.GetBackendDevice()->CreateBuffer( name, type, m_MaxVertices * m_VertexSize, dynamic); // create sole free chunk VBChunk* chunk = new VBChunk; chunk->m_Owner = this; chunk->m_Count = m_FreeVertices; chunk->m_Index = 0; m_FreeList.emplace_back(chunk); } CVertexBuffer::~CVertexBuffer() { // Must have released all chunks before destroying the buffer ENSURE(m_AllocList.empty()); m_Buffer.reset(); for (VBChunk* const& chunk : m_FreeList) delete chunk; } bool CVertexBuffer::CompatibleVertexType( const size_t vertexSize, const Renderer::Backend::GL::CBuffer::Type type, const bool dynamic) const { ENSURE(m_Buffer); return type == m_Buffer->GetType() && dynamic == m_Buffer->IsDynamic() && vertexSize == m_VertexSize; } /////////////////////////////////////////////////////////////////////////////// // Allocate: try to allocate a buffer of given number of vertices (each of // given size), with the given type, and using the given texture - return null // if no free chunks available CVertexBuffer::VBChunk* CVertexBuffer::Allocate( const size_t vertexSize, const size_t numberOfVertices, const Renderer::Backend::GL::CBuffer::Type type, const bool dynamic, void* backingStore) { // check this is the right kind of buffer if (!CompatibleVertexType(vertexSize, type, dynamic)) return nullptr; if (UseStreaming(dynamic)) ENSURE(backingStore != nullptr); // quick check there's enough vertices spare to allocate if (numberOfVertices > m_FreeVertices) return nullptr; // trawl free list looking for first free chunk with enough space std::vector::iterator best_iter = m_FreeList.end(); for (std::vector::iterator iter = m_FreeList.begin(); iter != m_FreeList.end(); ++iter) { if (numberOfVertices == (*iter)->m_Count) { best_iter = iter; break; } else if (numberOfVertices < (*iter)->m_Count && (best_iter == m_FreeList.end() || (*best_iter)->m_Count < (*iter)->m_Count)) best_iter = iter; } // We could not find a large enough chunk. if (best_iter == m_FreeList.end()) return nullptr; VBChunk* chunk = *best_iter; m_FreeList.erase(best_iter); m_FreeVertices -= chunk->m_Count; chunk->m_BackingStore = backingStore; chunk->m_Dirty = false; chunk->m_Needed = false; // split chunk into two; - allocate a new chunk using all unused vertices in the // found chunk, and add it to the free list if (chunk->m_Count > numberOfVertices) { VBChunk* newchunk = new VBChunk; newchunk->m_Owner = this; newchunk->m_Count = chunk->m_Count - numberOfVertices; newchunk->m_Index = chunk->m_Index + numberOfVertices; m_FreeList.emplace_back(newchunk); m_FreeVertices += newchunk->m_Count; // resize given chunk chunk->m_Count = numberOfVertices; } // return found chunk m_AllocList.push_back(chunk); return chunk; } /////////////////////////////////////////////////////////////////////////////// // Release: return given chunk to this buffer void CVertexBuffer::Release(VBChunk* chunk) { // Update total free count before potentially modifying this chunk's count m_FreeVertices += chunk->m_Count; m_AllocList.erase(std::find(m_AllocList.begin(), m_AllocList.end(), chunk)); // Sorting O(nlogn) shouldn't be too far from O(n) by performance, because // the container is partly sorted already. std::sort( m_FreeList.begin(), m_FreeList.end(), [](const VBChunk* chunk1, const VBChunk* chunk2) -> bool { return chunk1->m_Index < chunk2->m_Index; }); // Coalesce with any free-list items that are adjacent to this chunk; // merge the found chunk with the new one, and remove the old one // from the list. for (std::vector::iterator iter = m_FreeList.begin(); iter != m_FreeList.end();) { if ((*iter)->m_Index == chunk->m_Index + chunk->m_Count || (*iter)->m_Index + (*iter)->m_Count == chunk->m_Index) { chunk->m_Index = std::min(chunk->m_Index, (*iter)->m_Index); chunk->m_Count += (*iter)->m_Count; delete *iter; iter = m_FreeList.erase(iter); if (!m_FreeList.empty() && iter != m_FreeList.begin()) iter = std::prev(iter); } else { ++iter; } } m_FreeList.emplace_back(chunk); } /////////////////////////////////////////////////////////////////////////////// // UpdateChunkVertices: update vertex data for given chunk void CVertexBuffer::UpdateChunkVertices(VBChunk* chunk, void* data) { ENSURE(m_Buffer); if (UseStreaming(m_Buffer->IsDynamic())) { - // The VBO is now out of sync with the backing store + // The backend buffer is now out of sync with the backing store. chunk->m_Dirty = true; // Sanity check: Make sure the caller hasn't tried to reallocate - // their backing store + // their backing store. ENSURE(data == chunk->m_BackingStore); } else { ENSURE(data); g_Renderer.GetDeviceCommandContext()->UploadBufferRegion( m_Buffer.get(), data, chunk->m_Index * m_VertexSize, chunk->m_Count * m_VertexSize); } } void CVertexBuffer::UploadIfNeeded( Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext) { if (UseStreaming(m_Buffer->IsDynamic())) { if (!m_HasNeededChunks) return; - // If any chunks are out of sync with the current VBO, and are - // needed for rendering this frame, we'll need to re-upload the VBO + // If any chunks are out of sync with the current backend buffer, and are + // needed for rendering this frame, we'll need to re-upload the backend buffer. bool needUpload = false; for (VBChunk* const& chunk : m_AllocList) { if (chunk->m_Dirty && chunk->m_Needed) { needUpload = true; break; } } if (needUpload) { deviceCommandContext->UploadBuffer(m_Buffer.get(), [&](u8* mappedData) { #ifndef NDEBUG // To help detect bugs where PrepareForRendering() was not called, // force all not-needed data to 0, so things won't get rendered // with undefined (but possibly still correct-looking) data. memset(mappedData, 0, m_MaxVertices * m_VertexSize); #endif // Copy only the chunks we need. (This condition is helpful when - // the VBO contains data for every unit in the world, but only a - // handful are visible on screen and we don't need to bother copying - // the rest.) + // the backend buffer contains data for every unit in the world, + // but only a handful are visible on screen and we don't need to + // bother copying the rest.) for (VBChunk* const& chunk : m_AllocList) if (chunk->m_Needed) std::memcpy(mappedData + chunk->m_Index * m_VertexSize, chunk->m_BackingStore, chunk->m_Count * m_VertexSize); }); // Anything we just uploaded is clean; anything else is dirty - // since the rest of the VBO content is now undefined + // since the rest of the backend buffer content is now undefined for (VBChunk* const& chunk : m_AllocList) { if (chunk->m_Needed) { chunk->m_Dirty = false; chunk->m_Needed = false; } else chunk->m_Dirty = true; } } else { // Reset the flags for the next phase. for (VBChunk* const& chunk : m_AllocList) chunk->m_Needed = false; } m_HasNeededChunks = false; } } size_t CVertexBuffer::GetBytesReserved() const { return MAX_VB_SIZE_BYTES; } size_t CVertexBuffer::GetBytesAllocated() const { return (m_MaxVertices - m_FreeVertices) * m_VertexSize; } void CVertexBuffer::DumpStatus() const { debug_printf("freeverts = %d\n", static_cast(m_FreeVertices)); size_t maxSize = 0; for (VBChunk* const& chunk : m_FreeList) { debug_printf("free chunk %p: size=%d\n", static_cast(chunk), static_cast(chunk->m_Count)); maxSize = std::max(chunk->m_Count, maxSize); } debug_printf("max size = %d\n", static_cast(maxSize)); } bool CVertexBuffer::UseStreaming(const bool dynamic) { return dynamic; } void CVertexBuffer::PrepareForRendering(VBChunk* chunk) { chunk->m_Needed = true; m_HasNeededChunks = true; } Index: ps/trunk/source/renderer/VertexBuffer.h =================================================================== --- ps/trunk/source/renderer/VertexBuffer.h (revision 26834) +++ ps/trunk/source/renderer/VertexBuffer.h (revision 26835) @@ -1,162 +1,163 @@ /* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* - * encapsulation of VBOs with batching and sharing + * Encapsulation of backend buffers with batching and sharing. */ #ifndef INCLUDED_VERTEXBUFFER #define INCLUDED_VERTEXBUFFER #include "renderer/backend/gl/Buffer.h" #include "renderer/backend/gl/DeviceCommandContext.h" #include #include /** - * CVertexBuffer: encapsulation of ARB_vertex_buffer_object, also supplying + * CVertexBuffer: encapsulation of backend buffers, also supplying * some additional functionality for sharing buffers between multiple objects. * * The class can be used in two modes, depending on the usage parameter: * * Static buffer: Call Allocate() with backingStore = nullptr. Then call * UpdateChunkVertices() with any pointer - the data will be immediately copied - * to the VBO. This should be used for vertex data that rarely changes. + * to the buffer. This should be used for vertex data that rarely changes. * * Dynamic buffer: Call Allocate() with backingStore pointing * at some memory that will remain valid for the lifetime of the CVertexBuffer. * This should be used for vertex data that may change every frame. * Rendering is expected to occur in two phases: * - "Prepare" phase: * If this chunk is going to be used for rendering during the next rendering phase, * you must call PrepareForRendering(). * If the vertex data in backingStore has been modified since the last uploading phase, * you must call UpdateChunkVertices(). * - "Upload" phase: * UploadedIfNeeded() can be called (multiple times). The vertex data will be uploaded * to the GPU if necessary. * It is okay to have multiple prepare/upload cycles per frame (though slightly less * efficient), but they must occur sequentially. */ class CVertexBuffer { NONCOPYABLE(CVertexBuffer); public: - /// VBChunk: describes a portion of this vertex buffer + // VBChunk: describes a portion of this vertex buffer struct VBChunk { - /// Owning (parent) vertex buffer + // Owning (parent) vertex buffer CVertexBuffer* m_Owner; - /// Start index of this chunk in owner + // Start index of this chunk in owner size_t m_Index; - /// Number of vertices used by chunk + // Number of vertices used by chunk size_t m_Count; - /// If UseStreaming() is true, points at the data for this chunk + // If UseStreaming() is true, points at the data for this chunk void* m_BackingStore; - /// If true, the VBO is not consistent with the chunk's backing store - /// (and will need to be re-uploaded before rendering with this chunk) + // If true, the backend buffer is not consistent with the chunk's + // backing store (and will need to be re-uploaded before rendering with + // this chunk). bool m_Dirty; - /// If true, we have been told this chunk is going to be used for - /// rendering in the next uploading phase and will need to be uploaded + // If true, we have been told this chunk is going to be used for + // rendering in the next uploading phase and will need to be uploaded bool m_Needed; private: // Only CVertexBuffer can construct/delete these - // (Other people should use g_VBMan.Allocate, g_VBMan.Release) + // (Other people should use g_VBMan.AllocateChunk) friend class CVertexBuffer; VBChunk() {} ~VBChunk() {} }; public: // constructor, destructor CVertexBuffer( const char* name, const size_t vertexSize, const Renderer::Backend::GL::CBuffer::Type type, const bool dynamic); CVertexBuffer( const char* name, const size_t vertexSize, const Renderer::Backend::GL::CBuffer::Type type, const bool dynamic, const size_t maximumBufferSize); ~CVertexBuffer(); void UploadIfNeeded(Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext); /// Make the vertex data available for the next usage. void PrepareForRendering(VBChunk* chunk); /// Update vertex data for given chunk. Transfers the provided data to the actual OpenGL vertex buffer. void UpdateChunkVertices(VBChunk* chunk, void* data); size_t GetVertexSize() const { return m_VertexSize; } size_t GetBytesReserved() const; size_t GetBytesAllocated() const; /// Returns true if this vertex buffer is compatible with the specified vertex type and intended usage. bool CompatibleVertexType( const size_t vertexSize, const Renderer::Backend::GL::CBuffer::Type type, const bool dynamic) const; void DumpStatus() const; /** * Given the usage flags of a buffer that has been (or will be) allocated: * * If true, we assume the buffer is going to be modified on every frame, * so we will re-upload the entire buffer every frame using glMapBuffer. * This requires the buffer's owner to hold onto its backing store. * * If false, we assume it will change rarely, and use direct upload to * update it incrementally. The backing store can be freed to save memory. */ static bool UseStreaming(const bool dynamic); Renderer::Backend::GL::CBuffer* GetBuffer() { return m_Buffer.get(); } private: friend class CVertexBufferManager; // allow allocate only via CVertexBufferManager /// Try to allocate a buffer of given number of vertices (each of given size), /// and with the given type - return null if no free chunks available VBChunk* Allocate( const size_t vertexSize, const size_t numberOfVertices, const Renderer::Backend::GL::CBuffer::Type type, const bool dynamic, void* backingStore); /// Return given chunk to this buffer void Release(VBChunk* chunk); /// Vertex size of this vertex buffer size_t m_VertexSize; /// Number of vertices of above size in this buffer size_t m_MaxVertices; /// List of free chunks in this buffer std::vector m_FreeList; /// List of allocated chunks std::vector m_AllocList; /// Available free vertices - total of all free vertices in the free list size_t m_FreeVertices; std::unique_ptr m_Buffer; bool m_HasNeededChunks; }; #endif // INCLUDED_VERTEXBUFFER Index: ps/trunk/source/renderer/VertexBufferManager.cpp =================================================================== --- ps/trunk/source/renderer/VertexBufferManager.cpp (revision 26834) +++ ps/trunk/source/renderer/VertexBufferManager.cpp (revision 26835) @@ -1,209 +1,208 @@ /* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "VertexBufferManager.h" #include "lib/ogl.h" #include "ps/CLogger.h" -#include "ps/CStr.h" #define DUMP_VB_STATS 0 // for debugging namespace { const char* GetBufferTypeName( const Renderer::Backend::GL::CBuffer::Type type) { const char* name = "UnknownBuffer"; switch (type) { case Renderer::Backend::GL::CBuffer::Type::VERTEX: name = "VertexBuffer"; break; case Renderer::Backend::GL::CBuffer::Type::INDEX: name = "IndexBuffer"; break; default: debug_warn("Unknown buffer type"); break; } return name; } const char* GetGroupName( const CVertexBufferManager::Group group) { const char* name = "Unknown"; switch (group) { case CVertexBufferManager::Group::DEFAULT: name = "Default"; break; case CVertexBufferManager::Group::TERRAIN: name = "Terrain"; break; case CVertexBufferManager::Group::WATER: name = "Water"; break; default: debug_warn("Unknown buffer group"); break; } return name; } } // anonymous namespace CVertexBufferManager g_VBMan; CVertexBufferManager::Handle::Handle(Handle&& other) : m_Chunk(other.m_Chunk) { other.m_Chunk = nullptr; } CVertexBufferManager::Handle& CVertexBufferManager::Handle::operator=(Handle&& other) { if (&other == this) return *this; if (IsValid()) Reset(); Handle tmp(std::move(other)); swap(*this, tmp); return *this; } CVertexBufferManager::Handle::Handle(CVertexBuffer::VBChunk* chunk) : m_Chunk(chunk) { } void CVertexBufferManager::Handle::Reset() { if (!IsValid()) return; g_VBMan.Release(m_Chunk); m_Chunk = nullptr; } // Explicit shutdown of the vertex buffer subsystem. // This avoids the ordering issues that arise when using destructors of // global instances. void CVertexBufferManager::Shutdown() { for (int group = static_cast(Group::DEFAULT); group < static_cast(Group::COUNT); ++group) m_Buffers[group].clear(); } /** * AllocateChunk: try to allocate a buffer of given number of vertices (each of * given size), with the given type, and using the given texture - return null * if no free chunks available */ CVertexBufferManager::Handle CVertexBufferManager::AllocateChunk( const size_t vertexSize, const size_t numberOfVertices, const Renderer::Backend::GL::CBuffer::Type type, const bool dynamic, void* backingStore, Group group) { ENSURE(vertexSize > 0); ENSURE(numberOfVertices > 0); CVertexBuffer::VBChunk* result = nullptr; if (CVertexBuffer::UseStreaming(dynamic)) ENSURE(backingStore != NULL); // TODO, RC - run some sanity checks on allocation request std::vector>& buffers = m_Buffers[static_cast(group)]; #if DUMP_VB_STATS debug_printf("\n============================\n# allocate vsize=%zu nverts=%zu\n\n", vertexSize, numVertices); for (const std::unique_ptr& buffer : buffers) { if (buffer->CompatibleVertexType(vertexSize, type, dynamic)) { debug_printf("%p\n", buffer.get()); buffer->DumpStatus(); } } #endif // iterate through all existing buffers testing for one that'll // satisfy the allocation for (const std::unique_ptr& buffer : buffers) { result = buffer->Allocate(vertexSize, numberOfVertices, type, dynamic, backingStore); if (result) return Handle(result); } char bufferName[64] = {0}; snprintf( bufferName, std::size(bufferName), "%s (%s, %zu%s)", GetBufferTypeName(type), GetGroupName(group), vertexSize, (dynamic ? ", dynamic" : "")); // got this far; need to allocate a new buffer buffers.emplace_back( group == Group::DEFAULT ? std::make_unique(bufferName, vertexSize, type, dynamic) // Reduces the buffer size for not so frequent buffers. : std::make_unique(bufferName, vertexSize, type, dynamic, 1024 * 1024)); result = buffers.back()->Allocate(vertexSize, numberOfVertices, type, dynamic, backingStore); if (!result) { - LOGERROR("Failed to create VBOs (%zu*%zu)", vertexSize, numberOfVertices); + LOGERROR("Failed to create backend buffer (%zu*%zu)", vertexSize, numberOfVertices); return Handle(); } return Handle(result); } void CVertexBufferManager::Release(CVertexBuffer::VBChunk* chunk) { ENSURE(chunk); #if DUMP_VB_STATS debug_printf("\n============================\n# release %p nverts=%zu\n\n", chunk, chunk->m_Count); #endif chunk->m_Owner->Release(chunk); } size_t CVertexBufferManager::GetBytesReserved() const { size_t total = 0; for (int group = static_cast(Group::DEFAULT); group < static_cast(Group::COUNT); ++group) for (const std::unique_ptr& buffer : m_Buffers[static_cast(group)]) total += buffer->GetBytesReserved(); return total; } size_t CVertexBufferManager::GetBytesAllocated() const { size_t total = 0; for (int group = static_cast(Group::DEFAULT); group < static_cast(Group::COUNT); ++group) for (const std::unique_ptr& buffer : m_Buffers[static_cast(group)]) total += buffer->GetBytesAllocated(); return total; }