浏览代码

Merge pull request #802 from ZilverBlade/799-box-shadow-caching

Cache for box shadows
Michael R. P. Ragazzon 3 周之前
父节点
当前提交
5ee88dd7b3

+ 10 - 1
Include/RmlUi/Core/RenderBox.h

@@ -76,6 +76,15 @@ private:
 	EdgeSizes border_widths;
 	CornerSizes border_radius;
 };
-
+inline bool operator==(const RenderBox& a, const RenderBox& b)
+{
+	return a.GetFillSize() == b.GetFillSize() && a.GetBorderOffset() == b.GetBorderOffset() && a.GetBorderWidths() == b.GetBorderWidths() &&
+		a.GetBorderRadius() == b.GetBorderRadius();
+}
+inline bool operator!=(const RenderBox& a, const RenderBox& b)
+{
+	return !(a == b);
+}
 } // namespace Rml
+
 #endif

+ 125 - 0
Source/Core/BoxShadowCache.cpp

@@ -0,0 +1,125 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019-2023 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "BoxShadowCache.h"
+#include "../../Include/RmlUi/Core/ComputedValues.h"
+#include "../../Include/RmlUi/Core/MeshUtilities.h"
+#include "../../Include/RmlUi/Core/Profiling.h"
+#include "../../Include/RmlUi/Core/RenderManager.h"
+#include "../Core/ControlledLifetimeResource.h"
+#include "BoxShadowHash.h"
+#include "GeometryBoxShadow.h"
+
+namespace Rml {
+
+struct BoxShadowCacheData {
+	StableUnorderedMap<BoxShadowGeometryInfo, WeakPtr<BoxShadowRenderable>> handles;
+};
+
+static void ReleaseHandle(BoxShadowRenderable* handle);
+
+BoxShadowRenderable::BoxShadowRenderable(const BoxShadowGeometryInfo& geometry_info) : cache_key(geometry_info) {}
+
+BoxShadowRenderable::~BoxShadowRenderable()
+{
+	ReleaseHandle(this);
+}
+
+static ControlledLifetimeResource<BoxShadowCacheData> shadow_cache_data;
+
+void BoxShadowCache::Initialize()
+{
+	shadow_cache_data.Initialize();
+}
+
+void BoxShadowCache::Shutdown()
+{
+	shadow_cache_data.Shutdown();
+}
+
+static SharedPtr<BoxShadowRenderable> GetOrCreateBoxShadow(RenderManager& render_manager, const BoxShadowGeometryInfo& info)
+{
+	RMLUI_ZoneScoped;
+	auto it_handle = shadow_cache_data->handles.find(info);
+	if (it_handle != shadow_cache_data->handles.end())
+	{
+		SharedPtr<BoxShadowRenderable> result = it_handle->second.lock();
+		RMLUI_ASSERTMSG(result, "Failed to lock handle in Box Shadow cache");
+		return result;
+	}
+
+	const auto iterator_inserted = shadow_cache_data->handles.emplace(info, WeakPtr<BoxShadowRenderable>());
+	RMLUI_ASSERTMSG(iterator_inserted.second, "Could not insert entry into the Box Shadow cache handle map, duplicate key.");
+	const BoxShadowGeometryInfo& inserted_key = iterator_inserted.first->first;
+	WeakPtr<BoxShadowRenderable>& inserted_weak_data_pointer = iterator_inserted.first->second;
+
+	auto shadow_handle = MakeShared<BoxShadowRenderable>(inserted_key);
+	GeometryBoxShadow::GenerateTexture(shadow_handle->texture, shadow_handle->background_border_geometry, render_manager, inserted_key);
+
+	Mesh mesh;
+	const byte alpha = byte(info.opacity * 255.f);
+	MeshUtilities::GenerateQuad(mesh, -info.element_offset_in_texture, Vector2f(info.texture_dimensions), ColourbPremultiplied(alpha, alpha));
+	shadow_handle->geometry = render_manager.MakeGeometry(std::move(mesh));
+
+	inserted_weak_data_pointer = shadow_handle;
+	return shadow_handle;
+}
+
+static void ReleaseHandle(BoxShadowRenderable* handle)
+{
+	// There are no longer any users of the cache entry uniquely identified by the handle address. Start from the
+	// tip (i.e. per-color data) and remove that entry from its parent. Move up the cache ancestry and erase any
+	// entries that no longer have any children.
+	auto& handles = shadow_cache_data->handles;
+	const BoxShadowGeometryInfo& key = handle->cache_key;
+
+	auto it_handle = handles.find(key);
+	RMLUI_ASSERT(it_handle != handles.cend());
+
+	handles.erase(it_handle);
+}
+
+SharedPtr<BoxShadowRenderable> BoxShadowCache::GetHandle(Element* element, const ComputedValues& computed)
+{
+	RenderManager* render_manager = element->GetRenderManager();
+	if (!render_manager)
+		return {};
+
+	ColourbPremultiplied background_color = computed.background_color().ToPremultiplied();
+	Array<ColourbPremultiplied, 4> border_colors = {
+		computed.border_top_color().ToPremultiplied(),
+		computed.border_right_color().ToPremultiplied(),
+		computed.border_bottom_color().ToPremultiplied(),
+		computed.border_left_color().ToPremultiplied(),
+	};
+	const CornerSizes border_radius = computed.border_radius();
+	BoxShadowGeometryInfo geom_info = GeometryBoxShadow::Resolve(element, border_radius, background_color, border_colors, computed.opacity());
+	return GetOrCreateBoxShadow(*render_manager, geom_info);
+}
+
+} // namespace Rml

+ 65 - 0
Source/Core/BoxShadowCache.h

@@ -0,0 +1,65 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019-2025 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef RMLUI_CORE_BOXSHADOWCACHE_H
+#define RMLUI_CORE_BOXSHADOWCACHE_H
+
+#include "../../Include/RmlUi/Core/CallbackTexture.h"
+#include "../../Include/RmlUi/Core/Geometry.h"
+#include "../../Include/RmlUi/Core/Types.h"
+
+namespace Rml {
+namespace Style {
+	class ComputedValues;
+}
+struct BoxShadowGeometryInfo;
+
+struct BoxShadowRenderable : NonCopyMoveable {
+	BoxShadowRenderable(const BoxShadowGeometryInfo& geometry_info);
+	~BoxShadowRenderable();
+
+	CallbackTexture texture;
+	Geometry geometry;
+	Geometry background_border_geometry;
+	const BoxShadowGeometryInfo& cache_key;
+};
+
+class BoxShadowCache {
+public:
+	static void Initialize();
+	static void Shutdown();
+
+	/// Returns a handle to BoxShadow renderable matching the element's style - creates new data if none is found.
+	/// @param[in] element Element for which to calculate and cache the box shadow.
+	/// @param[in] computed The computed style values of the element.
+	/// @return A handle to the BoxShadow data, with automatic reference counting.
+	static SharedPtr<BoxShadowRenderable> GetHandle(Element* element, const Style::ComputedValues& computed);
+};
+
+} // namespace Rml
+#endif

+ 174 - 0
Source/Core/BoxShadowHash.h

@@ -0,0 +1,174 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019-2025 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef RMLUI_CORE_BOXSHADOWHASH_H
+#define RMLUI_CORE_BOXSHADOWHASH_H
+
+#include "../../Include/RmlUi/Core/ComputedValues.h"
+#include "../../Include/RmlUi/Core/NumericValue.h"
+#include "../../Include/RmlUi/Core/RenderBox.h"
+#include "../../Include/RmlUi/Core/Types.h"
+#include "../../Include/RmlUi/Core/Unit.h"
+#include "../../Include/RmlUi/Core/Utilities.h"
+#include "BoxShadowCache.h"
+#include "GeometryBoxShadow.h"
+
+namespace std {
+
+template <>
+struct hash<::Rml::Unit> {
+	using utype = underlying_type_t<::Rml::Unit>;
+	size_t operator()(const ::Rml::Unit& t) const noexcept
+	{
+		hash<utype> h;
+		return h(static_cast<utype>(t));
+	}
+};
+
+template <>
+struct hash<::Rml::Vector2i> {
+	size_t operator()(const ::Rml::Vector2i& v) const noexcept
+	{
+		using namespace ::Rml::Utilities;
+		size_t seed = hash<int>{}(v.x);
+		HashCombine(seed, v.y);
+		return seed;
+	}
+};
+template <>
+struct hash<::Rml::Vector2f> {
+	size_t operator()(const ::Rml::Vector2f& v) const noexcept
+	{
+		using namespace ::Rml::Utilities;
+		size_t seed = hash<float>{}(v.x);
+		HashCombine(seed, v.y);
+		return seed;
+	}
+};
+template <>
+struct hash<::Rml::Colourb> {
+	size_t operator()(const ::Rml::Colourb& v) const noexcept { return static_cast<size_t>(hash<uint32_t>{}(reinterpret_cast<const uint32_t&>(v))); }
+};
+template <>
+struct hash<::Rml::ColourbPremultiplied> {
+	size_t operator()(const ::Rml::ColourbPremultiplied& v) const noexcept
+	{
+		return static_cast<size_t>(hash<uint32_t>{}(reinterpret_cast<const uint32_t&>(v)));
+	}
+};
+
+template <>
+struct hash<::Rml::NumericValue> {
+	size_t operator()(const ::Rml::NumericValue& v) const noexcept
+	{
+		using namespace ::Rml::Utilities;
+		size_t seed = hash<float>{}(v.number);
+		HashCombine(seed, v.unit);
+		return seed;
+	}
+};
+
+template <>
+struct hash<::Rml::BoxShadow> {
+	size_t operator()(const ::Rml::BoxShadow& s) const noexcept
+	{
+		using namespace ::Rml;
+		using namespace ::Rml::Utilities;
+		size_t seed = std::hash<ColourbPremultiplied>{}(s.color);
+
+		HashCombine(seed, s.offset_x);
+		HashCombine(seed, s.offset_y);
+		HashCombine(seed, s.blur_radius);
+		HashCombine(seed, s.spread_distance);
+		HashCombine(seed, s.inset);
+		return seed;
+	}
+};
+
+template <>
+struct hash<::Rml::RenderBox> {
+	size_t operator()(const ::Rml::RenderBox& box) const noexcept
+	{
+		using namespace ::Rml::Utilities;
+		static auto HashArray4 = [](const ::Rml::Array<float, 4>& arr) -> size_t {
+			size_t seed = 0;
+			for (const auto& v : arr)
+				HashCombine(seed, v);
+			return seed;
+		};
+
+		size_t seed = 0;
+		HashCombine(seed, box.GetFillSize());
+		HashCombine(seed, box.GetBorderOffset());
+		HashCombine(seed, HashArray4(box.GetBorderRadius()));
+		HashCombine(seed, HashArray4(box.GetBorderWidths()));
+		return seed;
+	}
+};
+
+template <>
+struct hash<::Rml::BoxShadowGeometryInfo> {
+	size_t operator()(const ::Rml::BoxShadowGeometryInfo& in) const noexcept
+	{
+		using namespace ::Rml::Utilities;
+		size_t seed = size_t(849128392);
+
+		HashCombine(seed, in.background_color);
+		for (const auto& v : in.border_colors)
+		{
+			HashCombine(seed, v);
+		}
+
+		for (const auto& v : in.border_radius)
+		{
+			HashCombine(seed, v);
+		}
+
+		HashCombine(seed, in.texture_dimensions);
+		HashCombine(seed, in.element_offset_in_texture);
+
+		for (const auto& v : in.padding_render_boxes)
+		{
+			HashCombine(seed, v);
+		}
+		for (const auto& v : in.border_render_boxes)
+		{
+			HashCombine(seed, v);
+		}
+		for (const ::Rml::BoxShadow& v : in.shadow_list)
+		{
+			HashCombine(seed, v);
+		}
+		HashCombine(seed, in.opacity);
+		return seed;
+	}
+};
+
+} // namespace std
+
+#endif

+ 3 - 0
Source/Core/CMakeLists.txt

@@ -4,6 +4,9 @@
 add_library(rmlui_core
 	BaseXMLParser.cpp
 	Box.cpp
+	BoxShadowCache.h
+	BoxShadowCache.cpp
+	BoxShadowHash.h
 	CallbackTexture.cpp
 	Clock.cpp
 	Clock.h

+ 4 - 0
Source/Core/Core.cpp

@@ -40,6 +40,7 @@
 #include "../../Include/RmlUi/Core/SystemInterface.h"
 #include "../../Include/RmlUi/Core/TextInputHandler.h"
 #include "../../Include/RmlUi/Core/Types.h"
+#include "BoxShadowCache.h"
 #include "ComputeProperty.h"
 #include "ControlledLifetimeResource.h"
 #include "ElementMeta.h"
@@ -174,6 +175,7 @@ bool Initialise()
 #ifdef RMLUI_SVG_PLUGIN
 	SVG::Initialise();
 #endif
+	BoxShadowCache::Initialize();
 
 	// Notify all plugins we're starting up.
 	PluginRegistry::NotifyInitialise();
@@ -193,6 +195,8 @@ void Shutdown()
 	// Notify all plugins we're being shutdown.
 	PluginRegistry::NotifyShutdown();
 
+	BoxShadowCache::Shutdown();
+
 	Factory::Shutdown();
 	TemplateCache::Shutdown();
 	StyleSheetFactory::Shutdown();

+ 31 - 39
Source/Core/ElementBackgroundBorder.cpp

@@ -33,7 +33,9 @@
 #include "../../Include/RmlUi/Core/DecorationTypes.h"
 #include "../../Include/RmlUi/Core/Element.h"
 #include "../../Include/RmlUi/Core/MeshUtilities.h"
+#include "../../Include/RmlUi/Core/Profiling.h"
 #include "../../Include/RmlUi/Core/RenderManager.h"
+#include "BoxShadowCache.h"
 #include "GeometryBoxShadow.h"
 
 namespace Rml {
@@ -56,12 +58,14 @@ void ElementBackgroundBorder::Render(Element* element)
 		border_dirty = false;
 	}
 
-	Background* shadow = GetBackground(BackgroundType::BoxShadow);
-	if (shadow && shadow->geometry)
-		shadow->geometry.Render(element->GetAbsoluteOffset(BoxArea::Border), shadow->texture);
+	if (Background* shadow = GetBackground(BackgroundType::BoxShadowAndBackgroundBorder))
+	{
+		const Vector2f offset = element->GetAbsoluteOffset(BoxArea::Border);
+		shadow->box_shadow_and_background_border->geometry.Render(offset, shadow->box_shadow_and_background_border->texture);
+	}
 	else if (Background* background = GetBackground(BackgroundType::BackgroundBorder))
 	{
-		auto offset = element->GetAbsoluteOffset(BoxArea::Border);
+		const Vector2f offset = element->GetAbsoluteOffset(BoxArea::Border);
 		background->geometry.Render(offset);
 	}
 }
@@ -117,60 +121,48 @@ ElementBackgroundBorder::Background& ElementBackgroundBorder::GetOrCreateBackgro
 	return background;
 }
 
+void ElementBackgroundBorder::EraseBackground(BackgroundType type)
+{
+	backgrounds.erase(type);
+}
+
 void ElementBackgroundBorder::GenerateGeometry(Element* element)
 {
+	RMLUI_ZoneScoped;
 	RenderManager* render_manager = element->GetRenderManager();
 	if (!render_manager)
 		return;
 
 	const ComputedValues& computed = element->GetComputedValues();
 	const bool has_box_shadow = computed.has_box_shadow();
-	const float opacity = computed.opacity();
 
-	// Apply opacity except if we have a box shadow. In the latter case the background is rendered opaquely into the box-shadow texture, while
-	// opacity is applied to the entire box-shadow texture when that is rendered.
-	bool apply_opacity = (!has_box_shadow && opacity < 1.f);
+	if (has_box_shadow)
+	{
+		// The box shadow geometry also includes the element's background and border, thus we can skip the normal background generation.
+		EraseBackground(BackgroundType::BackgroundBorder);
+		Background& shadow_background = GetOrCreateBackground(BackgroundType::BoxShadowAndBackgroundBorder);
+		shadow_background.box_shadow_and_background_border = BoxShadowCache::GetHandle(element, computed);
+		return;
+	}
 
-	auto ConvertColor = [=](Colourb color) {
-		if (apply_opacity)
-			return color.ToPremultiplied(opacity);
-		else
-			return color.ToPremultiplied();
-	};
+	EraseBackground(BackgroundType::BoxShadowAndBackgroundBorder);
 
-	ColourbPremultiplied background_color = ConvertColor(computed.background_color());
-	ColourbPremultiplied border_colors[4] = {
-		ConvertColor(computed.border_top_color()),
-		ConvertColor(computed.border_right_color()),
-		ConvertColor(computed.border_bottom_color()),
-		ConvertColor(computed.border_left_color()),
+	const float opacity = computed.opacity();
+	ColourbPremultiplied background_color = computed.background_color().ToPremultiplied(opacity);
+	Array<ColourbPremultiplied, 4> border_colors = {
+		computed.border_top_color().ToPremultiplied(opacity),
+		computed.border_right_color().ToPremultiplied(opacity),
+		computed.border_bottom_color().ToPremultiplied(opacity),
+		computed.border_left_color().ToPremultiplied(opacity),
 	};
-	const CornerSizes border_radius = computed.border_radius();
 
 	Geometry& geometry = GetOrCreateBackground(BackgroundType::BackgroundBorder).geometry;
 	Mesh mesh = geometry.Release(Geometry::ReleaseMode::ClearMesh);
 
 	for (int i = 0; i < element->GetNumBoxes(); i++)
-		MeshUtilities::GenerateBackgroundBorder(mesh, element->GetRenderBox(BoxArea::Padding, i), background_color, border_colors);
+		MeshUtilities::GenerateBackgroundBorder(mesh, element->GetRenderBox(BoxArea::Padding, i), background_color, border_colors.data());
 
 	geometry = render_manager->MakeGeometry(std::move(mesh));
-
-	if (has_box_shadow)
-	{
-		Geometry& background_border_geometry = geometry;
-
-		const Property* p_box_shadow = element->GetLocalProperty(PropertyId::BoxShadow);
-		RMLUI_ASSERT(p_box_shadow->value.GetType() == Variant::BOXSHADOWLIST);
-		BoxShadowList shadow_list = p_box_shadow->value.Get<BoxShadowList>();
-
-		// Generate the geometry for the box-shadow texture.
-		Background& shadow_background = GetOrCreateBackground(BackgroundType::BoxShadow);
-		Geometry& shadow_geometry = shadow_background.geometry;
-		CallbackTexture& shadow_texture = shadow_background.texture;
-
-		GeometryBoxShadow::Generate(shadow_geometry, shadow_texture, *render_manager, element, background_border_geometry, std::move(shadow_list),
-			border_radius, computed.opacity());
-	}
 }
 
 } // namespace Rml

+ 6 - 3
Source/Core/ElementBackgroundBorder.h

@@ -35,10 +35,11 @@
 
 namespace Rml {
 
+struct BoxShadowRenderable;
+
 class ElementBackgroundBorder {
 public:
 	ElementBackgroundBorder();
-
 	void Render(Element* element);
 
 	void DirtyBackground();
@@ -47,14 +48,16 @@ public:
 	Geometry* GetClipGeometry(Element* element, BoxArea clip_area);
 
 private:
-	enum class BackgroundType { BackgroundBorder, BoxShadow, ClipBorder, ClipPadding, ClipContent, Count };
+	enum class BackgroundType { BackgroundBorder, BoxShadowAndBackgroundBorder, ClipBorder, ClipPadding, ClipContent, Count };
 	struct Background {
 		Geometry geometry;
-		CallbackTexture texture;
+		Texture texture;
+		SharedPtr<BoxShadowRenderable> box_shadow_and_background_border;
 	};
 
 	Background* GetBackground(BackgroundType type);
 	Background& GetOrCreateBackground(BackgroundType type);
+	void EraseBackground(BackgroundType type);
 
 	void GenerateGeometry(Element* element);
 

+ 68 - 30
Source/Core/GeometryBoxShadow.cpp

@@ -34,17 +34,24 @@
 #include "../../Include/RmlUi/Core/Geometry.h"
 #include "../../Include/RmlUi/Core/Math.h"
 #include "../../Include/RmlUi/Core/MeshUtilities.h"
+#include "../../Include/RmlUi/Core/Profiling.h"
 #include "../../Include/RmlUi/Core/RenderManager.h"
 
 namespace Rml {
 
-void GeometryBoxShadow::Generate(Geometry& out_shadow_geometry, CallbackTexture& out_shadow_texture, RenderManager& render_manager, Element* element,
-	Geometry& background_border_geometry, BoxShadowList shadow_list, const CornerSizes border_radius, const float opacity)
+BoxShadowGeometryInfo GeometryBoxShadow::Resolve(Element* element, const CornerSizes& border_radius, ColourbPremultiplied background_color,
+	const Array<ColourbPremultiplied, 4>& border_colors, float opacity)
 {
+	RMLUI_ZoneScoped;
+
 	// Find the box-shadow texture dimension and offset required to cover all box-shadows and element boxes combined.
 	Vector2f element_offset_in_texture;
 	Vector2i texture_dimensions;
 
+	const Property* p_box_shadow = element->GetLocalProperty(PropertyId::BoxShadow);
+	RMLUI_ASSERT(p_box_shadow->value.GetType() == Variant::BOXSHADOWLIST);
+	BoxShadowList shadow_list = p_box_shadow->value.Get<BoxShadowList>();
+
 	// Resolve all lengths to px units.
 	for (BoxShadow& shadow : shadow_list)
 	{
@@ -86,10 +93,48 @@ void GeometryBoxShadow::Generate(Geometry& out_shadow_geometry, CallbackTexture&
 		texture_dimensions = Vector2i(texture_region.Size());
 	}
 
+	// Since we can reuse textures across multiple box shadows with the same properties,
+	// we need to copy the element's box shadow list and the background and border geometry.
+	RenderBoxList padding_render_boxes{};
+	RenderBoxList border_render_boxes{};
+
+	for (int i = 0; i < element->GetNumBoxes(); i++)
+	{
+		padding_render_boxes.push_back(element->GetRenderBox(BoxArea::Padding, i));
+		border_render_boxes.push_back(element->GetRenderBox(BoxArea::Border, i));
+	}
+
+	// Finally, create cache information
+	BoxShadowGeometryInfo geometry_info;
+	geometry_info.background_color = background_color;
+	geometry_info.border_colors = border_colors;
+	geometry_info.border_radius = border_radius;
+	geometry_info.texture_dimensions = texture_dimensions;
+	geometry_info.element_offset_in_texture = element_offset_in_texture;
+	geometry_info.padding_render_boxes = std::move(padding_render_boxes);
+	geometry_info.border_render_boxes = std::move(border_render_boxes);
+	geometry_info.shadow_list = std::move(shadow_list);
+	geometry_info.opacity = opacity;
+	return geometry_info;
+}
+
+void GeometryBoxShadow::GenerateTexture(CallbackTexture& out_shadow_texture, Geometry& out_background_border_geometry, RenderManager& render_manager,
+	const BoxShadowGeometryInfo& info)
+{
+	RMLUI_ZoneScoped;
+
+	Mesh mesh = out_background_border_geometry.Release(Geometry::ReleaseMode::ClearMesh);
+	for (size_t i = 0; i < info.padding_render_boxes.size(); i++)
+		MeshUtilities::GenerateBackgroundBorder(mesh, info.padding_render_boxes[i], info.background_color, info.border_colors.data());
+	out_background_border_geometry = render_manager.MakeGeometry(std::move(mesh));
+
 	// Callback for generating the box-shadow texture. Using a callback ensures that the texture can be regenerated at any time, for example if the
 	// device loses its GPU context and the client calls Rml::ReleaseTextures().
-	auto texture_callback = [&background_border_geometry, element, border_radius, texture_dimensions, element_offset_in_texture,
-								shadow_list = std::move(shadow_list)](const CallbackTextureInterface& texture_interface) -> bool {
+	auto texture_callback = [&info, &out_background_border_geometry](const CallbackTextureInterface& texture_interface) -> bool {
+		RMLUI_ASSERT(info.border_render_boxes.size() == info.padding_render_boxes.size());
+		RMLUI_ZoneScopedN("BoxShadow::GenerateTexture::Callback");
+		size_t num_boxes = info.border_render_boxes.size();
+
 		RenderManager& render_manager = texture_interface.GetRenderManager();
 
 		Mesh mesh_padding;        // Render geometry for inner box-shadow.
@@ -97,7 +142,7 @@ void GeometryBoxShadow::Generate(Geometry& out_shadow_geometry, CallbackTexture&
 
 		bool has_inner_shadow = false;
 		bool has_outer_shadow = false;
-		for (const BoxShadow& shadow : shadow_list)
+		for (const BoxShadow& shadow : info.shadow_list)
 		{
 			if (shadow.inset)
 				has_inner_shadow = true;
@@ -106,18 +151,18 @@ void GeometryBoxShadow::Generate(Geometry& out_shadow_geometry, CallbackTexture&
 		}
 
 		// Generate the geometry for all the element's boxes and extend the render-texture further to cover all of them.
-		for (int i = 0; i < element->GetNumBoxes(); i++)
+		for (size_t i = 0; i < num_boxes; i++)
 		{
 			ColourbPremultiplied white(255);
 			if (has_inner_shadow)
-				MeshUtilities::GenerateBackground(mesh_padding, element->GetRenderBox(BoxArea::Padding, i), white);
+				MeshUtilities::GenerateBackground(mesh_padding, info.padding_render_boxes[i], white);
 			if (has_outer_shadow)
-				MeshUtilities::GenerateBackground(mesh_padding_border, element->GetRenderBox(BoxArea::Border, i), white);
+				MeshUtilities::GenerateBackground(mesh_padding_border, info.border_render_boxes[i], white);
 		}
 
 		const RenderState initial_render_state = render_manager.GetState();
 		render_manager.ResetState();
-		render_manager.SetScissorRegion(Rectanglei::FromSize(texture_dimensions));
+		render_manager.SetScissorRegion(Rectanglei::FromSize(info.texture_dimensions));
 
 		// The scissor region will be clamped to the current window size, check the resulting scissor region.
 		const Rectanglei scissor_region = render_manager.GetScissorRegion();
@@ -128,27 +173,26 @@ void GeometryBoxShadow::Generate(Geometry& out_shadow_geometry, CallbackTexture&
 			render_manager.SetState(initial_render_state);
 			return false;
 		}
-		if (scissor_region != Rectanglei::FromSize(texture_dimensions))
+		if (scissor_region != Rectanglei::FromSize(info.texture_dimensions))
 		{
 			Log::Message(Log::LT_INFO,
 				"The desired box-shadow texture dimensions (%d, %d) are larger than the current window region (%d, %d). "
-				"Results may be clipped. In element: %s",
-				texture_dimensions.x, texture_dimensions.y, scissor_region.Width(), scissor_region.Height(), element->GetAddress().c_str());
+				"Results may be clipped.",
+				info.texture_dimensions.x, info.texture_dimensions.y, scissor_region.Width(), scissor_region.Height());
 		}
 
 		render_manager.PushLayer();
+		out_background_border_geometry.Render(info.element_offset_in_texture);
 
-		background_border_geometry.Render(element_offset_in_texture);
-
-		for (int shadow_index = (int)shadow_list.size() - 1; shadow_index >= 0; shadow_index--)
+		for (int shadow_index = (int)info.shadow_list.size() - 1; shadow_index >= 0; shadow_index--)
 		{
-			const BoxShadow& shadow = shadow_list[shadow_index];
+			const BoxShadow& shadow = info.shadow_list[shadow_index];
 			const Vector2f shadow_offset = {shadow.offset_x.number, shadow.offset_y.number};
 			const bool inset = shadow.inset;
 			const float spread_distance = shadow.spread_distance.number;
 			const float blur_radius = shadow.blur_radius.number;
 
-			CornerSizes spread_radii = border_radius;
+			CornerSizes spread_radii = info.border_radius;
 			for (int i = 0; i < 4; i++)
 			{
 				float& radius = spread_radii[i];
@@ -164,10 +208,10 @@ void GeometryBoxShadow::Generate(Geometry& out_shadow_geometry, CallbackTexture&
 			Mesh mesh_shadow;
 
 			// Generate the shadow geometry. For outer box-shadows it is rendered normally, while for inset box-shadows it is used as a clipping mask.
-			for (int i = 0; i < element->GetNumBoxes(); i++)
+			for (size_t i = 0; i < num_boxes; i++)
 			{
 				const float signed_spread_distance = (inset ? -spread_distance : spread_distance);
-				RenderBox render_box = element->GetRenderBox(inset ? BoxArea::Padding : BoxArea::Border, i);
+				RenderBox render_box = (inset ? info.padding_render_boxes : info.border_render_boxes)[i];
 				render_box.SetFillSize(Math::Max(render_box.GetFillSize() + Vector2f(2.f * signed_spread_distance), Vector2f{0.001f}));
 				render_box.SetBorderRadius(spread_radii);
 				render_box.SetBorderOffset(render_box.GetBorderOffset() - Vector2f(signed_spread_distance));
@@ -186,23 +230,23 @@ void GeometryBoxShadow::Generate(Geometry& out_shadow_geometry, CallbackTexture&
 
 			if (inset)
 			{
-				render_manager.SetClipMask(ClipMaskOperation::SetInverse, &geometry_shadow, shadow_offset + element_offset_in_texture);
+				render_manager.SetClipMask(ClipMaskOperation::SetInverse, &geometry_shadow, shadow_offset + info.element_offset_in_texture);
 
 				for (Rml::Vertex& vertex : mesh_padding.vertices)
 					vertex.colour = shadow.color;
 
 				// @performance: Don't need to copy the mesh if this is the last use of it.
 				Geometry geometry_padding = render_manager.MakeGeometry(Mesh(mesh_padding));
-				geometry_padding.Render(element_offset_in_texture);
+				geometry_padding.Render(info.element_offset_in_texture);
 
-				render_manager.SetClipMask(ClipMaskOperation::Set, &geometry_padding, element_offset_in_texture);
+				render_manager.SetClipMask(ClipMaskOperation::Set, &geometry_padding, info.element_offset_in_texture);
 			}
 			else
 			{
 				Mesh mesh = mesh_padding_border;
 				Geometry geometry_padding_border = render_manager.MakeGeometry(std::move(mesh));
-				render_manager.SetClipMask(ClipMaskOperation::SetInverse, &geometry_padding_border, element_offset_in_texture);
-				geometry_shadow.Render(shadow_offset + element_offset_in_texture);
+				render_manager.SetClipMask(ClipMaskOperation::SetInverse, &geometry_padding_border, info.element_offset_in_texture);
+				geometry_shadow.Render(shadow_offset + info.element_offset_in_texture);
 			}
 
 			if (blur)
@@ -223,12 +267,6 @@ void GeometryBoxShadow::Generate(Geometry& out_shadow_geometry, CallbackTexture&
 		return true;
 	};
 
-	Mesh mesh = out_shadow_geometry.Release(Geometry::ReleaseMode::ClearMesh);
-	const byte alpha = byte(opacity * 255.f);
-	MeshUtilities::GenerateQuad(mesh, -element_offset_in_texture, Vector2f(texture_dimensions), ColourbPremultiplied(alpha, alpha));
-
 	out_shadow_texture = render_manager.MakeCallbackTexture(std::move(texture_callback));
-	out_shadow_geometry = render_manager.MakeGeometry(std::move(mesh));
 }
-
 } // namespace Rml

+ 40 - 10
Source/Core/GeometryBoxShadow.h

@@ -29,10 +29,34 @@
 #ifndef RMLUI_CORE_GEOMETRYBOXSHADOW_H
 #define RMLUI_CORE_GEOMETRYBOXSHADOW_H
 
+#include "../../Include/RmlUi/Core/DecorationTypes.h"
 #include "../../Include/RmlUi/Core/RenderBox.h"
 #include "../../Include/RmlUi/Core/Types.h"
 
 namespace Rml {
+using RenderBoxList = Vector<RenderBox>;
+struct BoxShadowGeometryInfo {
+	ColourbPremultiplied background_color;
+	Array<ColourbPremultiplied, 4> border_colors;
+	CornerSizes border_radius;
+	Vector2i texture_dimensions;
+	Vector2f element_offset_in_texture;
+	RenderBoxList padding_render_boxes;
+	RenderBoxList border_render_boxes;
+	BoxShadowList shadow_list;
+	float opacity;
+};
+inline bool operator==(const BoxShadowGeometryInfo& a, const BoxShadowGeometryInfo& b)
+{
+	return a.background_color == b.background_color && a.border_colors == b.border_colors && a.border_radius == b.border_radius &&
+		a.texture_dimensions == b.texture_dimensions && a.element_offset_in_texture == b.element_offset_in_texture &&
+		a.padding_render_boxes == b.padding_render_boxes && a.border_render_boxes == b.border_render_boxes && a.shadow_list == b.shadow_list &&
+		a.opacity == b.opacity;
+}
+inline bool operator!=(const BoxShadowGeometryInfo& a, const BoxShadowGeometryInfo& b)
+{
+	return !(a == b);
+}
 
 class Geometry;
 class CallbackTexture;
@@ -40,19 +64,25 @@ class RenderManager;
 
 class GeometryBoxShadow {
 public:
-	/// Generate the texture and geometry for a box shadow.
-	/// @param[out] out_shadow_geometry The target geometry.
+	/// Resolve the element's properties into its box geometry info.
+	/// @param[in] element The element to resolve.
+	/// @param[in] border_radius The border radius of the element.
+	/// @param[in] background_color The background colour of the element.
+	/// @param[in] border_colors The border colours of the element.
+	/// @param[in] opacity The computed opacity of the element.
+	static BoxShadowGeometryInfo Resolve(Element* element, const CornerSizes& border_radius, ColourbPremultiplied background_color,
+		const Array<ColourbPremultiplied, 4>& border_colors, float opacity);
+
+	/// Generate the texture and geometry for a box shadow and including the element's background and border.
 	/// @param[out] out_shadow_texture The target texture, assumes pointer stability during the lifetime of the shadow geometry.
+	/// @param[out] out_background_border_geometry The generated geometry for the element's background and border.
 	/// @param[in] render_manager The render manager to generate the shadow for.
-	/// @param[in] element The element to generate the shadow for.
-	/// @param[in] background_border_geometry The geometry of the background and border, assumed to already have been generated. Assumes pointer
-	/// stability during the lifetime of the shadow geometry.
-	/// @param[in] shadow_list The list of box-shadows to generate.
-	/// @param[in] border_radius The border radius of the element.
-	/// @param[in] opacity The opacity of the element.
-	static void Generate(Geometry& out_shadow_geometry, CallbackTexture& out_shadow_texture, RenderManager& render_manager, Element* element,
-		Geometry& background_border_geometry, BoxShadowList shadow_list, CornerSizes border_radius, float opacity);
+	/// @param[in] shadow_geometry_info The resolved box shadow geometry of a given element.
+	///	@see GeometryBoxShadow::Resolve()
+	static void GenerateTexture(CallbackTexture& out_shadow_texture, Geometry& out_background_border_geometry, RenderManager& render_manager,
+		const BoxShadowGeometryInfo& shadow_geometry_info);
 };
 
 } // namespace Rml
+
 #endif

+ 92 - 6
Tests/Source/Benchmarks/BackgroundBorder.cpp

@@ -37,7 +37,12 @@
 using namespace ankerl;
 using namespace Rml;
 
-static String document_rml = R"(
+TEST_CASE("background_border")
+{
+	Context* context = TestsShell::GetContext();
+	REQUIRE(context);
+
+	static String document_rml = R"(
 <rml>
 <head>
     <link type="text/rcss" href="/../Tests/Data/style.rcss"/>
@@ -77,11 +82,6 @@ static String document_rml = R"(
 </rml>
 )";
 
-TEST_CASE("backgrounds_and_borders")
-{
-	Context* context = TestsShell::GetContext();
-	REQUIRE(context);
-
 	ElementDocument* document = context->LoadDocumentFromMemory(document_rml);
 	REQUIRE(document);
 	document->Show();
@@ -140,3 +140,89 @@ TEST_CASE("backgrounds_and_borders")
 
 	document->Close();
 }
+
+TEST_CASE("box_shadow")
+{
+	Context* context = TestsShell::GetContext();
+	REQUIRE(context);
+
+	static String document_rml = R"(
+<rml>
+<head>
+    <link type="text/rcss" href="/../Tests/Data/style.rcss"/>
+	<style>
+		#boxshadow > div {
+			width: 280dp;
+			height: 70dp;
+			border: 2dp #def6f7;
+			margin: 10dp auto;
+			padding: 15dp;
+			border-radius: 30dp 8dp;
+			box-sizing: border-box;
+			margin-top: 100px;
+			margin-bottom: 100px;
+		}
+		#boxshadow.blur > div {
+			box-shadow:
+				#f00f  40px  30px 25px 0px,
+				#00ff -40px -30px 45px 0px,
+				#0f08 -60px  70px 60px 0px,
+				#333a  0px  0px 30px 15px inset;
+		}
+	</style>
+</head>
+
+<body>
+<div id="boxshadow" class="blur">
+	<div/><div/><div/><div/><div/><div/><div/><div/><div/><div/>
+</div>
+</body>
+</rml>
+)";
+
+	ElementDocument* document = context->LoadDocumentFromMemory(document_rml);
+	REQUIRE(document);
+	document->Show();
+
+	nanobench::Bench bench;
+	bench.title("Box-shadow");
+	bench.relative(true);
+	bench.warmup(5);
+
+	TestsShell::RenderLoop(true);
+
+	Element* element_boxshadow = document->GetElementById("boxshadow");
+
+	ElementList elements;
+	document->QuerySelectorAll(elements, "#boxshadow > div");
+	REQUIRE(!elements.empty());
+	bench.run("Reference (update + render)", [&] { TestsShell::RenderLoop(false); });
+
+	element_boxshadow->SetClass("blur", true);
+	bench.run("Box-shadow (repeated)", [&] {
+		// Force regeneration of backgrounds without changing layout
+		for (auto& element : elements)
+			element->SetProperty(Rml::PropertyId::BackgroundColor, Rml::Property(Colourb(), Unit::COLOUR));
+		TestsShell::RenderLoop(false);
+	});
+
+	unsigned int unique_id = 0;
+	element_boxshadow->SetClass("blur", false);
+	bench.run("Box-shadow (unique)", [&] {
+		for (Element* element : elements)
+		{
+			unique_id += 1;
+			String id_string = CreateString("%x", unique_id);
+			REQUIRE(id_string.size() < 12);
+			id_string.resize(12, 'f');
+			const auto id_index_to_color = [&](int color_index) { return id_string.substr(color_index * 3, 3); };
+			const String value =
+				CreateString("#%sf 40px 30px 25px 0px, #%sf -40px -30px 0px 0px, #%s8 -60px 70px 0px 0px, #%sa 0px 0px 30px 15px inset",
+					id_index_to_color(0).c_str(), id_index_to_color(1).c_str(), id_index_to_color(2).c_str(), id_index_to_color(3).c_str());
+			element->SetProperty("box-shadow", value);
+		}
+		TestsShell::RenderLoop(false);
+	});
+
+	document->Close();
+}