Prechádzať zdrojové kódy

Add clip mask to render interface, introduce render manager to keep track of the render state

- The clip mask can be rendered to using normal geometry, then during other render commands the clip mask should hide any contents outside its area.
- Improved element clipping behavior: Handles more complicated cases, including nested transforms with hidden overflow, and clips to the curved edge of elements with border-radius.
- Text culling now also considers the viewport and properly handles transforms. Previously, text was not rendered in some situations, or unnecessarily rendered outside the window.
- Clip mask implemented in GL3 renderer using stencil buffer.
- Added and modified visual tests for clipping behavior.
Michael Ragazzon 2 rokov pred
rodič
commit
69ea397f4f

+ 58 - 1
Backends/RmlUi_Renderer_GL3.cpp

@@ -978,6 +978,63 @@ void RenderInterface_GL3::SetScissorRegion(int x, int y, int width, int height)
 	SetScissor(Rml::Rectanglei::FromPositionSize({x, y}, {width, height}));
 }
 
+void RenderInterface_GL3::EnableClipMask(bool enable)
+{
+	if (enable)
+		glEnable(GL_STENCIL_TEST);
+	else
+		glDisable(GL_STENCIL_TEST);
+}
+
+void RenderInterface_GL3::RenderToClipMask(Rml::ClipMaskOperation operation, Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation)
+{
+	RMLUI_ASSERT(glIsEnabled(GL_STENCIL_TEST));
+	using Rml::ClipMaskOperation;
+
+	const bool clear_stencil = (operation == ClipMaskOperation::Set || operation == ClipMaskOperation::SetInverse);
+	if (clear_stencil)
+	{
+		// @performance Increment the reference value instead of clearing each time.
+		glClear(GL_STENCIL_BUFFER_BIT);
+	}
+
+	GLint stencil_test_value = 0;
+	glGetIntegerv(GL_STENCIL_REF, &stencil_test_value);
+
+	glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
+	glStencilFunc(GL_ALWAYS, GLint(1), GLuint(-1));
+
+	switch (operation)
+	{
+	case ClipMaskOperation::Set:
+	{
+		glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
+		stencil_test_value = 1;
+	}
+	break;
+	case ClipMaskOperation::SetInverse:
+	{
+		glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
+		stencil_test_value = 0;
+	}
+	break;
+	case ClipMaskOperation::Intersect:
+	{
+		glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);
+		stencil_test_value += 1;
+	}
+	break;
+	}
+
+	RenderCompiledGeometry(geometry, translation, {});
+
+	// Restore state
+	// @performance Cache state so we don't toggle it unnecessarily.
+	glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+	glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
+	glStencilFunc(GL_EQUAL, stencil_test_value, GLuint(-1));
+}
+
 // Set to byte packing, or the compiler will expand our struct, which means it won't read correctly from file
 #pragma pack(1)
 struct TGAHeader {
@@ -1293,7 +1350,7 @@ void RenderInterface_GL3::ReleaseTexture(Rml::TextureHandle texture_handle)
 
 void RenderInterface_GL3::SetTransform(const Rml::Matrix4f* new_transform)
 {
-	transform = projection * (new_transform ? *new_transform : Rml::Matrix4f::Identity());
+	transform = (new_transform ? (projection * (*new_transform)) : projection);
 	program_transform_dirty.set();
 }
 

+ 3 - 0
Backends/RmlUi_Renderer_GL3.h

@@ -72,6 +72,9 @@ public:
 	void EnableScissorRegion(bool enable) override;
 	void SetScissorRegion(int x, int y, int width, int height) override;
 
+	void EnableClipMask(bool enable) override;
+	void RenderToClipMask(Rml::ClipMaskOperation mask_operation, Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation) override;
+
 	bool LoadTexture(Rml::TextureHandle& texture_handle, Rml::Vector2i& texture_dimensions, const Rml::String& source) override;
 	bool GenerateTexture(Rml::TextureHandle& texture_handle, const Rml::byte* source, const Rml::Vector2i& source_dimensions) override;
 	void ReleaseTexture(Rml::TextureHandle texture_handle) override;

+ 2 - 0
CMake/FileList.cmake

@@ -192,6 +192,7 @@ set(Core_PUB_HDR_FILES
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/PropertySpecification.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Rectangle.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/RenderInterface.h
+    ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/RenderManager.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/ScriptInterface.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/ScrollTypes.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Spritesheet.h
@@ -360,6 +361,7 @@ set(Core_SRC_FILES
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserTransform.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertySpecification.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/RenderInterface.cpp
+    ${PROJECT_SOURCE_DIR}/Source/Core/RenderManager.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/ScrollController.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/Spritesheet.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/Stream.cpp

+ 3 - 0
Include/RmlUi/Config/Config.h

@@ -47,6 +47,7 @@
 	#include <queue>
 	#include <stack>
 	#include <string>
+	#include <map>
 	#include <unordered_map>
 	#include <utility>
 	#include <vector>
@@ -87,6 +88,8 @@ using Queue = std::queue<T>;
 template <typename T1, typename T2>
 using Pair = std::pair<T1, T2>;
 template <typename Key, typename Value>
+using StableMap = std::map<Key, Value>;
+template <typename Key, typename Value>
 using UnorderedMultimap = std::unordered_multimap<Key, Value>;
 
 	#ifdef RMLUI_NO_THIRDPARTY_CONTAINERS

+ 1 - 0
Include/RmlUi/Core.h

@@ -76,6 +76,7 @@
 #include "Core/PropertyParser.h"
 #include "Core/PropertySpecification.h"
 #include "Core/RenderInterface.h"
+#include "Core/RenderManager.h"
 #include "Core/Spritesheet.h"
 #include "Core/StringUtilities.h"
 #include "Core/StyleSheet.h"

+ 5 - 10
Include/RmlUi/Core/Context.h

@@ -31,6 +31,7 @@
 
 #include "Header.h"
 #include "Input.h"
+#include "RenderManager.h"
 #include "ScriptInterface.h"
 #include "ScrollTypes.h"
 #include "Traits.h"
@@ -246,14 +247,8 @@ public:
 	/// @param[in] speed_factor A factor for adjusting the final smooth scrolling speed, must be strictly positive, defaults to 1.0.
 	void SetDefaultScrollBehavior(ScrollBehavior scroll_behavior, float speed_factor);
 
-	/// Gets the current clipping region for the render traversal
-	/// @param[out] origin The clipping origin
-	/// @param[out] dimensions The clipping dimensions
-	bool GetActiveClipRegion(Vector2i& origin, Vector2i& dimensions) const;
-	/// Sets the current clipping region for the render traversal
-	/// @param[out] origin The clipping origin
-	/// @param[out] dimensions The clipping dimensions
-	void SetActiveClipRegion(Vector2i origin, Vector2i dimensions);
+	/// Retrieves the render manager which can be used to submit changes to the render state.
+	RenderManager& GetRenderManager();
 
 	/// Sets the instancer to use for releasing this object.
 	/// @param[in] instancer The context's instancer.
@@ -369,8 +364,8 @@ private:
 	// itself can't be part of it.
 	ElementSet drag_hover_chain;
 
-	Vector2i clip_origin;
-	Vector2i clip_dimensions;
+	// Wrapper around the render interface for tracking the render state.
+	RenderManager render_manager;
 
 	using DataModels = UnorderedMap<String, UniquePtr<DataModel>>;
 	DataModels data_models;

+ 3 - 0
Include/RmlUi/Core/Element.h

@@ -50,6 +50,7 @@ class Decorator;
 class ElementInstancer;
 class EventDispatcher;
 class EventListener;
+class ElementBackgroundBorder;
 class ElementDecoration;
 class ElementDefinition;
 class ElementDocument;
@@ -571,6 +572,8 @@ public:
 	EventDispatcher* GetEventDispatcher() const;
 	/// Returns event types with number of listeners for debugging.
 	String GetEventDispatcherSummary() const;
+	/// Access the element background and border.
+	ElementBackgroundBorder* GetElementBackgroundBorder() const;
 	/// Access the element decorators.
 	ElementDecoration* GetElementDecoration() const;
 	/// Returns the element's scrollbar functionality.

+ 10 - 11
Include/RmlUi/Core/ElementUtilities.h

@@ -30,6 +30,7 @@
 #define RMLUI_CORE_ELEMENTUTILITIES_H
 
 #include "Header.h"
+#include "RenderManager.h"
 #include "Types.h"
 
 namespace Rml {
@@ -87,19 +88,18 @@ public:
 	static int GetStringWidth(Element* element, const String& string, Character prior_character = Character::Null);
 
 	/// Generates the clipping region for an element.
-	/// @param[out] clip_origin The origin, in context coordinates, of the origin of the element's clipping window.
-	/// @param[out] clip_dimensions The size, in context coordinates, of the element's clipping window.
 	/// @param[in] element The element to generate the clipping region for.
+	/// @param[out] clip_region The element's clipping region in window coordinates.
+	/// @param[out] clip_mask_list Optional, returns a list of geometry that defines the element's clip mask.
+	/// @param[in] force_clip_self If true, also clips to the border area of the provided element regardless.
 	/// @return True if a clipping region exists for the element and clip_origin and clip_window were set, false if not.
-	static bool GetClippingRegion(Vector2i& clip_origin, Vector2i& clip_dimensions, Element* element);
+	static bool GetClippingRegion(Element* element, Rectanglei& clip_region, ClipMaskGeometryList* clip_mask_list = nullptr,
+		bool force_clip_self = false);
 	/// Sets the clipping region from an element and its ancestors.
 	/// @param[in] element The element to generate the clipping region from.
-	/// @param[in] context The context of the element; if this is not supplied, it will be derived from the element.
+	/// @param[in] force_clip_self If true, also clips to the border area of the provided element regardless.
 	/// @return The visibility of the given element within its clipping region.
-	static bool SetClippingRegion(Element* element, Context* context = nullptr);
-	/// Applies the clip region from the render interface to the renderer
-	/// @param[in] context The context to read the clip region from
-	static void ApplyActiveClipRegion(Context* context);
+	static bool SetClippingRegion(Element* element, bool force_clip_self = false);
 
 	/// Returns a rectangle covering the element's area in window coordinate space.
 	/// @param[in] out_rectangle The resulting rectangle covering the projected element's box.
@@ -129,9 +129,8 @@ public:
 	static bool PositionElement(Element* element, Vector2f offset, PositionAnchor anchor);
 
 	/// Applies an element's accumulated transform matrix, determined from its and ancestor's `perspective' and `transform' properties.
-	/// @param[in] element The element whose transform to apply.
-	/// @return true if a render interface is available to set the transform.
-	/// @note All calls to RenderInterface::SetTransform must go through here.
+	/// @param[in] element The element whose transform to apply, or nullptr for identity transform.
+	/// @return True if the transform could be submitted to the render interface.
 	static bool ApplyTransform(Element& element);
 
 	/// Creates data views and data controllers if a data model applies to the element.

+ 6 - 0
Include/RmlUi/Core/Geometry.h

@@ -38,6 +38,7 @@ namespace Rml {
 class Context;
 class Element;
 struct Texture;
+enum class ClipMaskOperation;
 using GeometryDatabaseHandle = uint32_t;
 
 /**
@@ -62,6 +63,11 @@ public:
 	/// @param[in] translation The translation of the geometry.
 	void Render(Vector2f translation);
 
+	/// Use the geometry to set the clip mask through the render interface. Requires that the geometry can be compiled.
+	/// @param[in] operation The clip mask operation to apply.
+	/// @param[in] translation The translation of the geometry.
+	void RenderToClipMask(ClipMaskOperation operation, Vector2f translation);
+
 	/// Returns the geometry's vertices. If these are written to, Release() should be called to force a recompile.
 	/// @return The geometry's vertex array.
 	Vector<Vertex>& GetVertices();

+ 15 - 0
Include/RmlUi/Core/RenderInterface.h

@@ -37,6 +37,11 @@
 
 namespace Rml {
 
+enum class ClipMaskOperation {
+	Set,        // Set the clip mask to the area of the rendered geometry, clearing any existing clip mask.
+	SetInverse, // Set the clip mask to the area *outside* the rendered geometry, clearing any existing clip mask.
+	Intersect,  // Intersect the clip mask with the area of the rendered geometry.
+};
 enum class LayerFill {
 	None,  // No operation necessary, does not care about the layer color.
 	Clear, // Clear the layer to transparent black.
@@ -102,6 +107,16 @@ public:
 	/// @param[in] height The height of the scissored region. All pixels to below (y + height) should be clipped.
 	virtual void SetScissorRegion(int x, int y, int width, int height) = 0;
 
+	/// Called by RmlUi when it wants to enable or disable the clip mask.
+	/// @param[in] enable True if the clip mask is to be enabled, false if it is to be disabled.
+	/// @note When enabled, the clip mask should hide any rendered contents outside the area of the mask.
+	virtual void EnableClipMask(bool enable);
+	/// Called by RmlUi when it wants to set or modify the contents of the clip mask.
+	/// @param[in] operation Describes how the geometry should affect the clip mask.
+	/// @param[in] geometry The compiled geometry to render.
+	/// @param[in] translation The translation to apply to the geometry.
+	virtual void RenderToClipMask(ClipMaskOperation operation, CompiledGeometryHandle geometry, Vector2f translation);
+
 	/// Called by RmlUi when a texture is required by the library.
 	/// @param[out] texture_handle The handle to write the texture handle for the loaded texture to.
 	/// @param[out] texture_dimensions The variable to write the dimensions of the loaded texture.

+ 100 - 0
Include/RmlUi/Core/RenderManager.h

@@ -0,0 +1,100 @@
+/*
+ * 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.
+ *
+ */
+
+#ifndef RMLUI_CORE_RENDERMANAGER_H
+#define RMLUI_CORE_RENDERMANAGER_H
+
+#include "Box.h"
+#include "RenderInterface.h"
+#include "Types.h"
+
+namespace Rml {
+
+class Geometry;
+
+struct ClipMaskGeometry {
+	ClipMaskOperation operation;
+	Geometry* geometry;
+	Vector2f absolute_offset;
+	const Matrix4f* transform;
+};
+inline bool operator==(const ClipMaskGeometry& a, const ClipMaskGeometry& b)
+{
+	return a.operation == b.operation && a.geometry == b.geometry && a.absolute_offset == b.absolute_offset && a.transform == b.transform;
+}
+inline bool operator!=(const ClipMaskGeometry& a, const ClipMaskGeometry& b)
+{
+	return !(a == b);
+}
+using ClipMaskGeometryList = Vector<ClipMaskGeometry>;
+
+struct RenderState {
+	Rectanglei scissor_region = Rectanglei::MakeInvalid();
+	ClipMaskGeometryList clip_mask_list;
+	Matrix4f transform = Matrix4f::Identity();
+};
+
+/**
+    A wrapper over the render interface which tracks the following state:
+       - Scissor
+       - Clip mask
+       - Transform
+    All such operations on the render interface should go through this class.
+ */
+class RMLUICORE_API RenderManager : NonCopyMoveable {
+public:
+	RenderManager();
+
+	void DisableScissorRegion();
+	void SetScissorRegion(Rectanglei region);
+
+	void DisableClipMask();
+	void SetClipMask(ClipMaskGeometryList clip_elements);
+	void SetClipMask(ClipMaskOperation operation, Geometry* geometry, Vector2f translation);
+
+	void SetTransform(const Matrix4f* new_transform);
+
+	const RenderState& GetState() const { return state; }
+	void SetState(const RenderState& next);
+	void ResetState();
+
+	void BeginRender();
+	void SetViewport(Vector2i dimensions);
+	Vector2i GetViewport() const;
+
+private:
+	void ApplyClipMask(const ClipMaskGeometryList& clip_elements);
+
+	RenderState state;
+	RenderInterface* render_interface = nullptr;
+	Vector2i viewport_dimensions;
+};
+
+} // namespace Rml
+
+#endif

+ 10 - 27
Source/Core/Context.cpp

@@ -55,8 +55,7 @@ static constexpr float DOUBLE_CLICK_MAX_DIST = 3.f; // [dp]
 static constexpr float UNIT_SCROLL_LENGTH = 80.f;   // [dp]
 
 Context::Context(const String& name) :
-	name(name), dimensions(0, 0), density_independent_pixel_ratio(1.0f), mouse_position(0, 0), clip_origin(-1, -1), clip_dimensions(-1, -1),
-	next_update_timeout(0)
+	name(name), dimensions(0, 0), density_independent_pixel_ratio(1.0f), mouse_position(0, 0), next_update_timeout(0)
 {
 	instancer = nullptr;
 
@@ -126,6 +125,7 @@ void Context::SetDimensions(const Vector2i _dimensions)
 	if (dimensions != _dimensions)
 	{
 		dimensions = _dimensions;
+		render_manager.SetViewport(dimensions);
 		root->SetBox(Box(Vector2f(dimensions)));
 		root->DirtyLayout();
 
@@ -141,8 +141,6 @@ void Context::SetDimensions(const Vector2i _dimensions)
 				document->DispatchEvent(EventId::Resize, Dictionary());
 			}
 		}
-
-		clip_dimensions = dimensions;
 	}
 }
 
@@ -218,12 +216,10 @@ bool Context::Render()
 {
 	RMLUI_ZoneScoped;
 
-	ElementUtilities::ApplyActiveClipRegion(this);
+	render_manager.BeginRender();
 
 	root->Render();
 
-	ElementUtilities::SetClippingRegion(nullptr, this);
-
 	// Render the cursor proxy so that any attached drag clone will be rendered below the cursor.
 	if (drag_clone)
 	{
@@ -233,6 +229,8 @@ bool Context::Render()
 		cursor_proxy->Render();
 	}
 
+	render_manager.ResetState();
+
 	return true;
 }
 
@@ -855,21 +853,9 @@ void Context::SetDefaultScrollBehavior(ScrollBehavior scroll_behavior, float spe
 	scroll_controller->SetDefaultScrollBehavior(scroll_behavior, speed_factor);
 }
 
-bool Context::GetActiveClipRegion(Vector2i& origin, Vector2i& dimensions) const
-{
-	if (clip_dimensions.x < 0 || clip_dimensions.y < 0)
-		return false;
-
-	origin = clip_origin;
-	dimensions = clip_dimensions;
-
-	return true;
-}
-
-void Context::SetActiveClipRegion(const Vector2i origin, const Vector2i dimensions)
+RenderManager& Context::GetRenderManager()
 {
-	clip_origin = origin;
-	clip_dimensions = dimensions;
+	return render_manager;
 }
 
 void Context::SetInstancer(ContextInstancer* _instancer)
@@ -1224,12 +1210,9 @@ Element* Context::GetElementAtPoint(Vector2f point, const Element* ignore_elemen
 	if (within_element)
 	{
 		// The element may have been clipped out of view if it overflows an ancestor, so check its clipping region.
-		Vector2i clip_origin, clip_dimensions;
-		if (ElementUtilities::GetClippingRegion(clip_origin, clip_dimensions, element))
-		{
-			within_element = point.x >= clip_origin.x && point.y >= clip_origin.y && point.x <= (clip_origin.x + clip_dimensions.x) &&
-				point.y <= (clip_origin.y + clip_dimensions.y);
-		}
+		Rectanglei clip_region;
+		if (ElementUtilities::GetClippingRegion(element, clip_region))
+			within_element = clip_region.Contains(Vector2i(point));
 	}
 
 	if (within_element)

+ 5 - 0
Source/Core/Element.cpp

@@ -1541,6 +1541,11 @@ String Element::GetEventDispatcherSummary() const
 	return meta->event_dispatcher.ToString();
 }
 
+ElementBackgroundBorder* Element::GetElementBackgroundBorder() const
+{
+	return &meta->background_border;
+}
+
 ElementDecoration* Element::GetElementDecoration() const
 {
 	return &meta->decoration;

+ 45 - 5
Source/Core/ElementBackgroundBorder.cpp

@@ -40,14 +40,17 @@ void ElementBackgroundBorder::Render(Element* element)
 {
 	if (background_dirty || border_dirty)
 	{
+		for (auto& background : backgrounds)
+			background.second.geometry.Release(true);
+
 		GenerateGeometry(element);
 
 		background_dirty = false;
 		border_dirty = false;
 	}
 
-	if (geometry)
-		geometry.Render(element->GetAbsoluteOffset(BoxArea::Border));
+	if (Geometry* geometry = GetGeometry(BackgroundType::BackgroundBorder))
+		geometry->Render(element->GetAbsoluteOffset(BoxArea::Border));
 }
 
 void ElementBackgroundBorder::DirtyBackground()
@@ -60,6 +63,28 @@ void ElementBackgroundBorder::DirtyBorder()
 	border_dirty = true;
 }
 
+Geometry* ElementBackgroundBorder::GetClipGeometry(Element* element, BoxArea clip_area)
+{
+	BackgroundType type = {};
+	switch (clip_area)
+	{
+	case Rml::BoxArea::Border: type = BackgroundType::ClipBorder; break;
+	case Rml::BoxArea::Padding: type = BackgroundType::ClipPadding; break;
+	case Rml::BoxArea::Content: type = BackgroundType::ClipContent; break;
+	default: RMLUI_ERROR; return nullptr;
+	}
+
+	Geometry& geometry = GetOrCreateBackground(type).geometry;
+	if (!geometry)
+	{
+		const Box& box = element->GetBox();
+		const Vector4f border_radius = element->GetComputedValues().border_radius();
+		GeometryUtilities::GenerateBackground(&geometry, box, {}, border_radius, Colourb(255), clip_area);
+	}
+
+	return &geometry;
+}
+
 void ElementBackgroundBorder::GenerateGeometry(Element* element)
 {
 	const ComputedValues& computed = element->GetComputedValues();
@@ -83,8 +108,8 @@ void ElementBackgroundBorder::GenerateGeometry(Element* element)
 			border_colors[i].alpha = (byte)(opacity * (float)border_colors[i].alpha);
 	}
 
-	geometry.GetVertices().clear();
-	geometry.GetIndices().clear();
+	Geometry& geometry = GetOrCreateBackground(BackgroundType::BackgroundBorder).geometry;
+	RMLUI_ASSERT(!geometry);
 
 	for (int i = 0; i < element->GetNumBoxes(); i++)
 	{
@@ -92,8 +117,23 @@ void ElementBackgroundBorder::GenerateGeometry(Element* element)
 		const Box& box = element->GetBox(i, offset);
 		GeometryUtilities::GenerateBackgroundBorder(&geometry, box, offset, border_radius, background_color, border_colors);
 	}
+}
+
+Geometry* ElementBackgroundBorder::GetGeometry(BackgroundType type)
+{
+	auto it = backgrounds.find(type);
+	if (it != backgrounds.end())
+		return &it->second.geometry;
+	return nullptr;
+}
+
+ElementBackgroundBorder::Background& ElementBackgroundBorder::GetOrCreateBackground(BackgroundType type)
+{
+	auto it = backgrounds.find(type);
+	if (it != backgrounds.end())
+		return it->second;
 
-	geometry.Release();
+	return backgrounds.emplace(type, Background{}).first->second;
 }
 
 } // namespace Rml

+ 13 - 1
Source/Core/ElementBackgroundBorder.h

@@ -30,6 +30,7 @@
 #define RMLUI_CORE_ELEMENTBACKGROUNDBORDER_H
 
 #include "../../Include/RmlUi/Core/Geometry.h"
+#include "../../Include/RmlUi/Core/Texture.h"
 #include "../../Include/RmlUi/Core/Types.h"
 
 namespace Rml {
@@ -43,13 +44,24 @@ public:
 	void DirtyBackground();
 	void DirtyBorder();
 
+	Geometry* GetClipGeometry(Element* element, BoxArea clip_area);
+
 private:
+	enum class BackgroundType { BackgroundBorder, ClipBorder, ClipPadding, ClipContent, Count };
+	struct Background {
+		Geometry geometry;
+		Texture texture;
+	};
+
 	void GenerateGeometry(Element* element);
 
+	Geometry* GetGeometry(BackgroundType type);
+	Background& GetOrCreateBackground(BackgroundType type);
+
 	bool background_dirty = false;
 	bool border_dirty = false;
 
-	Geometry geometry;
+	StableMap<BackgroundType, Background> backgrounds;
 };
 
 } // namespace Rml

+ 17 - 16
Source/Core/ElementDecoration.cpp

@@ -208,14 +208,21 @@ void ElementDecoration::RenderDecorators(RenderStage render_stage)
 	if (!render_interface || !context)
 		return;
 
-	auto ApplyClippingRegion = [this, render_interface, context](bool extend_ink_overflow) {
-		ElementUtilities::SetClippingRegion(element); // TODO: For backdrop-filter only: Force clipping to our border-box.
+	RenderManager& render_manager = context->GetRenderManager();
+	Rectanglei initial_scissor_region = render_manager.GetState().scissor_region;
+
+	auto ApplyClippingRegion = [this, &render_manager, initial_scissor_region](PropertyId filter_id) {
+		RMLUI_ASSERT(filter_id == PropertyId::Filter || filter_id == PropertyId::BackdropFilter);
+
+		const bool force_clip_to_self_border_box = (filter_id == PropertyId::BackdropFilter);
+		ElementUtilities::SetClippingRegion(element, force_clip_to_self_border_box);
 
 		// Find the region being affected by the active filters and apply it as a scissor.
 		Rectanglef filter_region = Rectanglef::MakeInvalid();
 		ElementUtilities::GetBoundingBox(filter_region, element, BoxArea::Auto);
 
-		if (extend_ink_overflow)
+		// The filter property may draw outside our normal clipping region due to ink overflow.
+		if (filter_id == PropertyId::Filter)
 		{
 			for (const auto& filter : filters)
 				filter.filter->ExtendInkOverflow(element, filter_region);
@@ -223,22 +230,16 @@ void ElementDecoration::RenderDecorators(RenderStage render_stage)
 
 		Math::ExpandToPixelGrid(filter_region);
 
-		Rectanglei scissor_region = Rectanglei::FromSize(context->GetDimensions());
-		Vector2i clip_position, clip_size;
-		if (context->GetActiveClipRegion(clip_position, clip_size))
-			scissor_region.Intersect(Rectanglei::FromPositionSize(clip_position, clip_size));
-
-		scissor_region.IntersectIfValid(Rectanglei(filter_region));
-
-		render_interface->EnableScissorRegion(true);
-		render_interface->SetScissorRegion(scissor_region.Left(), scissor_region.Top(), scissor_region.Width(), scissor_region.Height());
+		Rectanglei scissor_region = Rectanglei(filter_region);
+		scissor_region.IntersectIfValid(initial_scissor_region);
+		render_manager.SetScissorRegion(scissor_region);
 	};
 
 	if (!backdrop_filters.empty())
 	{
 		if (render_stage == RenderStage::Enter)
 		{
-			ApplyClippingRegion(false);
+			ApplyClippingRegion(PropertyId::BackdropFilter);
 
 			render_interface->PushLayer(LayerFill::Clone);
 
@@ -251,7 +252,7 @@ void ElementDecoration::RenderDecorators(RenderStage render_stage)
 
 			render_interface->PopLayer(BlendMode::Replace, filter_handles);
 
-			ElementUtilities::ApplyActiveClipRegion(context);
+			render_manager.SetScissorRegion(initial_scissor_region);
 		}
 	}
 
@@ -263,7 +264,7 @@ void ElementDecoration::RenderDecorators(RenderStage render_stage)
 		}
 		else if (render_stage == RenderStage::Exit)
 		{
-			ApplyClippingRegion(true);
+			ApplyClippingRegion(PropertyId::Filter);
 
 			FilterHandleList filter_handles;
 			for (auto& filter : filters)
@@ -274,7 +275,7 @@ void ElementDecoration::RenderDecorators(RenderStage render_stage)
 
 			render_interface->PopLayer(BlendMode::Blend, filter_handles);
 
-			ElementUtilities::ApplyActiveClipRegion(context);
+			render_manager.SetScissorRegion(initial_scissor_region);
 		}
 	}
 }

+ 20 - 17
Source/Core/ElementText.cpp

@@ -36,9 +36,11 @@
 #include "../../Include/RmlUi/Core/GeometryUtilities.h"
 #include "../../Include/RmlUi/Core/Profiling.h"
 #include "../../Include/RmlUi/Core/Property.h"
+#include "../../Include/RmlUi/Core/RenderManager.h"
 #include "ComputeProperty.h"
 #include "ElementDefinition.h"
 #include "ElementStyle.h"
+#include "TransformState.h"
 
 namespace Rml {
 
@@ -136,30 +138,31 @@ void ElementText::OnRender()
 	const Vector2f translation = GetAbsoluteOffset();
 
 	bool render = true;
-	Vector2i clip_origin;
-	Vector2i clip_dimensions;
-	if (GetContext()->GetActiveClipRegion(clip_origin, clip_dimensions))
+	const RenderManager& render_manager = GetContext()->GetRenderManager();
+
+	// Do a visibility test against the scissor region to avoid unnecessary render calls. Instead of handling
+	// culling in complicated transform cases, for simplicity we always proceed to render if one is detected.
+	Rectanglei scissor_region = render_manager.GetState().scissor_region;
+	if (!scissor_region.Valid())
+		scissor_region = Rectanglei::FromSize(render_manager.GetViewport());
+
+	if (!GetTransformState() || !GetTransformState()->GetTransform())
 	{
 		const FontMetrics& font_metrics = GetFontEngineInterface()->GetFontMetrics(GetFontFaceHandle());
-		float clip_top = (float)clip_origin.y;
-		float clip_left = (float)clip_origin.x;
-		float clip_right = (float)(clip_origin.x + clip_dimensions.x);
-		float clip_bottom = (float)(clip_origin.y + clip_dimensions.y);
-		float ascent = font_metrics.ascent;
-		float descent = font_metrics.descent;
+		const int ascent = Math::RoundUpToInteger(font_metrics.ascent);
+		const int descent = Math::RoundUpToInteger(font_metrics.descent);
 
 		render = false;
 		for (const Line& line : lines)
 		{
-			float x_left = translation.x + line.position.x;
-			float x_right = x_left + line.width;
-			float y = translation.y + line.position.y;
-			float y_top = y - ascent;
-			float y_bottom = y + descent;
-
-			render = !(x_left > clip_right || x_right < clip_left || y_top > clip_bottom || y_bottom < clip_top);
-			if (render)
+			const Vector2i baseline = Vector2i(translation + line.position);
+			const Rectanglei line_region = Rectanglei::FromCorners(baseline - Vector2i(0, ascent), baseline + Vector2i(line.width, descent));
+
+			if (line_region.Valid() && scissor_region.Intersects(line_region))
+			{
+				render = true;
 				break;
+			}
 		}
 	}
 

+ 71 - 82
Source/Core/ElementUtilities.cpp

@@ -33,10 +33,13 @@
 #include "../../Include/RmlUi/Core/ElementScroll.h"
 #include "../../Include/RmlUi/Core/Factory.h"
 #include "../../Include/RmlUi/Core/FontEngineInterface.h"
+#include "../../Include/RmlUi/Core/Math.h"
 #include "../../Include/RmlUi/Core/RenderInterface.h"
+#include "../../Include/RmlUi/Core/RenderManager.h"
 #include "DataController.h"
 #include "DataModel.h"
 #include "DataView.h"
+#include "ElementBackgroundBorder.h"
 #include "ElementStyle.h"
 #include "Layout/LayoutDetails.h"
 #include "Layout/LayoutEngine.h"
@@ -164,14 +167,12 @@ int ElementUtilities::GetStringWidth(Element* element, const String& string, Cha
 	return GetFontEngineInterface()->GetStringWidth(font_face_handle, string, letter_spacing, prior_character);
 }
 
-bool ElementUtilities::GetClippingRegion(Vector2i& clip_origin, Vector2i& clip_dimensions, Element* element)
+bool ElementUtilities::GetClippingRegion(Element* element, Rectanglei& out_clip_region, ClipMaskGeometryList* out_clip_mask_list,
+	bool force_clip_self)
 {
 	using Style::Clip;
-	clip_origin = Vector2i(-1, -1);
-	clip_dimensions = Vector2i(-1, -1);
-
 	Clip target_element_clip = element->GetComputedValues().clip();
-	if (target_element_clip == Clip::Type::None)
+	if (target_element_clip == Clip::Type::None && !force_clip_self)
 		return false;
 
 	int num_ignored_clips = target_element_clip.GetNumber();
@@ -179,44 +180,59 @@ bool ElementUtilities::GetClippingRegion(Vector2i& clip_origin, Vector2i& clip_d
 	// Search through the element's ancestors, finding all elements that clip their overflow and have overflow to clip.
 	// For each that we find, we combine their clipping region with the existing clipping region, and so build up a
 	// complete clipping region for the element.
-	Element* clipping_element = element->GetOffsetParent();
+	Element* clipping_element = (force_clip_self ? element : element->GetParentNode());
+
+	Rectanglef clip_region = Rectanglef::MakeInvalid();
 
-	while (clipping_element != nullptr)
+	while (clipping_element)
 	{
+		const bool force_clip_current_element = (force_clip_self && clipping_element == element);
 		const ComputedValues& clip_computed = clipping_element->GetComputedValues();
 		const bool clip_enabled = (clip_computed.overflow_x() != Style::Overflow::Visible || clip_computed.overflow_y() != Style::Overflow::Visible);
 		const bool clip_always = (clip_computed.clip() == Clip::Type::Always);
 		const bool clip_none = (clip_computed.clip() == Clip::Type::None);
 		const int clip_number = clip_computed.clip().GetNumber();
 
-		// Merge the existing clip region with the current clip region if we aren't ignoring clip regions.
-		if ((clip_always || clip_enabled) && num_ignored_clips == 0)
+		// Merge the existing clip region with the current clip region, unless we are ignoring clip regions.
+		if (((clip_always || clip_enabled) && num_ignored_clips == 0) || force_clip_current_element)
 		{
-			// Ignore nodes that don't clip.
-			if (clip_always || clipping_element->GetClientWidth() < clipping_element->GetScrollWidth() - 0.5f ||
-				clipping_element->GetClientHeight() < clipping_element->GetScrollHeight() - 0.5f)
-			{
-				const BoxArea client_area = clipping_element->GetClientArea();
-				Vector2f element_origin_f = clipping_element->GetAbsoluteOffset(client_area);
-				Vector2f element_dimensions_f = clipping_element->GetBox().GetSize(client_area);
-				Math::SnapToPixelGrid(element_origin_f, element_dimensions_f);
+			const BoxArea client_area = (force_clip_current_element ? BoxArea::Border : clipping_element->GetClientArea());
+			const bool has_clipping_content =
+				(clip_always || force_clip_current_element || clipping_element->GetClientWidth() < clipping_element->GetScrollWidth() - 0.5f ||
+					clipping_element->GetClientHeight() < clipping_element->GetScrollHeight() - 0.5f);
+			bool disable_scissor_clipping = false;
 
-				const Vector2i element_origin(element_origin_f);
-				const Vector2i element_dimensions(element_dimensions_f);
-
-				if (clip_origin == Vector2i(-1, -1) && clip_dimensions == Vector2i(-1, -1))
+			if (out_clip_mask_list)
+			{
+				const TransformState* transform_state = clipping_element->GetTransformState();
+				const Matrix4f* transform = (transform_state ? transform_state->GetTransform() : nullptr);
+				const bool has_border_radius = (clip_computed.border_top_left_radius() > 0.f || clip_computed.border_top_right_radius() > 0.f ||
+					clip_computed.border_bottom_right_radius() > 0.f || clip_computed.border_bottom_left_radius() > 0.f);
+
+				// If the element has border-radius we always use a clip mask, since we can't easily predict if content is located on the curved
+				// region to be clipped. If the element has a transform we only use a clip mask when the content clips.
+				if (has_border_radius || (transform && has_clipping_content))
 				{
-					clip_origin = element_origin;
-					clip_dimensions = element_dimensions;
+					Geometry* clip_geometry = clipping_element->GetElementBackgroundBorder()->GetClipGeometry(clipping_element, client_area);
+					const ClipMaskOperation clip_operation = (out_clip_mask_list->empty() ? ClipMaskOperation::Set : ClipMaskOperation::Intersect);
+					const Vector2f absolute_offset = clipping_element->GetAbsoluteOffset(BoxArea::Border);
+					out_clip_mask_list->push_back(ClipMaskGeometry{clip_operation, clip_geometry, absolute_offset, transform});
 				}
-				else
-				{
-					const Vector2i top_left = Math::Max(clip_origin, element_origin);
-					const Vector2i bottom_right = Math::Min(clip_origin + clip_dimensions, element_origin + element_dimensions);
 
-					clip_origin = top_left;
-					clip_dimensions = Math::Max(Vector2i(0), bottom_right - top_left);
-				}
+				// If we only have border-radius then we add this element to the scissor region as well as the clip mask. This may help with e.g.
+				// culling text render calls. However, when we have a transform, the element cannot be added to the scissor region since its geometry
+				// may be projected entirely elsewhere.
+				if (transform)
+					disable_scissor_clipping = true;
+			}
+
+			if (has_clipping_content && !disable_scissor_clipping)
+			{
+				// Shrink the scissor region to the element's client area.
+				Vector2f element_offset = clipping_element->GetAbsoluteOffset(client_area);
+				Vector2f element_size = clipping_element->GetBox().GetSize(client_area);
+
+				clip_region.IntersectIfValid(Rectanglef::FromPositionSize(element_offset, element_size));
 			}
 		}
 
@@ -235,48 +251,35 @@ bool ElementUtilities::GetClippingRegion(Vector2i& clip_origin, Vector2i& clip_d
 		clipping_element = clipping_element->GetOffsetParent();
 	}
 
-	return clip_dimensions.x >= 0 && clip_dimensions.y >= 0;
+	if (clip_region.Valid())
+	{
+		Math::ExpandToPixelGrid(clip_region);
+		out_clip_region = Rectanglei(clip_region);
+	}
+
+	return clip_region.Valid();
 }
 
-bool ElementUtilities::SetClippingRegion(Element* element, Context* context)
+bool ElementUtilities::SetClippingRegion(Element* element, bool force_clip_self)
 {
-	if (element && !context)
-		context = element->GetContext();
-
+	Context* context = element->GetContext();
 	if (!context)
 		return false;
 
-	Vector2i clip_origin = {-1, -1};
-	Vector2i clip_dimensions = {-1, -1};
-	bool clip = element && GetClippingRegion(clip_origin, clip_dimensions, element);
+	RenderManager& render_manager = context->GetRenderManager();
 
-	Vector2i current_origin = {-1, -1};
-	Vector2i current_dimensions = {-1, -1};
-	bool current_clip = context->GetActiveClipRegion(current_origin, current_dimensions);
-	if (current_clip != clip || (clip && (clip_origin != current_origin || clip_dimensions != current_dimensions)))
-	{
-		context->SetActiveClipRegion(clip_origin, clip_dimensions);
-		ApplyActiveClipRegion(context);
-	}
-
-	return true;
-}
+	Rectanglei clip_region;
+	ClipMaskGeometryList clip_mask_list;
 
-void ElementUtilities::ApplyActiveClipRegion(Context* context)
-{
-	RenderInterface* render_interface = ::Rml::GetRenderInterface();
-	if (!render_interface)
-		return;
+	const bool scissoring_enabled = GetClippingRegion(element, clip_region, &clip_mask_list, force_clip_self);
+	if (scissoring_enabled)
+		render_manager.SetScissorRegion(clip_region);
+	else
+		render_manager.DisableScissorRegion();
 
-	Vector2i origin;
-	Vector2i dimensions;
-	bool clip_enabled = context->GetActiveClipRegion(origin, dimensions);
+	render_manager.SetClipMask(std::move(clip_mask_list));
 
-	render_interface->EnableScissorRegion(clip_enabled);
-	if (clip_enabled)
-	{
-		render_interface->SetScissorRegion(origin.x, origin.y, dimensions.x, dimensions.y);
-	}
+	return true;
 }
 
 bool ElementUtilities::GetBoundingBox(Rectanglef& out_rectangle, Element* element, BoxArea box_area)
@@ -377,31 +380,17 @@ bool ElementUtilities::PositionElement(Element* element, Vector2f offset, Positi
 
 bool ElementUtilities::ApplyTransform(Element& element)
 {
-	RenderInterface* render_interface = ::Rml::GetRenderInterface();
-	if (!render_interface)
+	Context* context = element.GetContext();
+	if (!context)
 		return false;
 
-	static const Matrix4f* old_transform_ptr = {}; // This may be expired, dereferencing not allowed!
-	static Matrix4f old_transform_value = Matrix4f::Identity();
+	RenderManager& render_manager = context->GetRenderManager();
 
-	const Matrix4f* new_transform_ptr = nullptr;
+	const Matrix4f* new_transform = nullptr;
 	if (const TransformState* state = element.GetTransformState())
-		new_transform_ptr = state->GetTransform();
-
-	// Only changed transforms are submitted.
-	if (old_transform_ptr != new_transform_ptr)
-	{
-		// Do a deep comparison as well to avoid submitting a new transform which is equal.
-		if (!old_transform_ptr || !new_transform_ptr || (old_transform_value != *new_transform_ptr))
-		{
-			render_interface->SetTransform(new_transform_ptr);
-
-			if (new_transform_ptr)
-				old_transform_value = *new_transform_ptr;
-		}
+		new_transform = state->GetTransform();
 
-		old_transform_ptr = new_transform_ptr;
-	}
+	render_manager.SetTransform(new_transform);
 
 	return true;
 }

+ 24 - 1
Source/Core/Geometry.cpp

@@ -75,7 +75,7 @@ Geometry::~Geometry()
 
 void Geometry::Render(Vector2f translation)
 {
-	RenderInterface* const render_interface = ::Rml::GetRenderInterface();
+	RenderInterface* render_interface = GetRenderInterface();
 	RMLUI_ASSERT(render_interface);
 
 	translation = translation.Round();
@@ -116,6 +116,29 @@ void Geometry::Render(Vector2f translation)
 	}
 }
 
+void Geometry::RenderToClipMask(ClipMaskOperation clip_mask, Vector2f translation)
+{
+	RenderInterface* render_interface = GetRenderInterface();
+	RMLUI_ASSERT(render_interface);
+
+	if (!compile_attempted)
+	{
+		if (vertices.empty() || indices.empty())
+			return;
+
+		RMLUI_ZoneScoped;
+
+		compile_attempted = true;
+		compiled_geometry = render_interface->CompileGeometry(&vertices[0], (int)vertices.size(), &indices[0], (int)indices.size());
+	}
+
+	if (compiled_geometry)
+	{
+		translation = translation.Round();
+		render_interface->RenderToClipMask(clip_mask, compiled_geometry, translation);
+	}
+}
+
 Vector<Vertex>& Geometry::GetVertices()
 {
 	return vertices;

+ 4 - 0
Source/Core/RenderInterface.cpp

@@ -51,6 +51,10 @@ void RenderInterface::RenderCompiledGeometry(CompiledGeometryHandle /*geometry*/
 
 void RenderInterface::ReleaseCompiledGeometry(CompiledGeometryHandle /*geometry*/) {}
 
+void RenderInterface::EnableClipMask(bool /*enable*/) {}
+
+void RenderInterface::RenderToClipMask(ClipMaskOperation /*operation*/, CompiledGeometryHandle /*geometry*/, Vector2f /*translation*/) {}
+
 bool RenderInterface::LoadTexture(TextureHandle& /*texture_handle*/, Vector2i& /*texture_dimensions*/, const String& /*source*/)
 {
 	return false;

+ 156 - 0
Source/Core/RenderManager.cpp

@@ -0,0 +1,156 @@
+/*
+ * 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 "../../Include/RmlUi/Core/RenderManager.h"
+#include "../../Include/RmlUi/Core/Core.h"
+#include "../../Include/RmlUi/Core/Geometry.h"
+#include "../../Include/RmlUi/Core/RenderInterface.h"
+
+namespace Rml {
+
+RenderManager::RenderManager() : render_interface(GetRenderInterface())
+{
+	RMLUI_ASSERT(render_interface);
+}
+
+void RenderManager::BeginRender()
+{
+#ifdef RMLUI_DEBUG
+	const RenderState default_state;
+	RMLUI_ASSERT(state.clip_mask_list == default_state.clip_mask_list);
+	RMLUI_ASSERT(state.scissor_region == state.scissor_region);
+	RMLUI_ASSERT(state.transform == state.transform);
+#endif
+}
+
+void RenderManager::SetViewport(Vector2i dimensions)
+{
+	viewport_dimensions = dimensions;
+}
+
+Vector2i RenderManager::GetViewport() const
+{
+	return viewport_dimensions;
+}
+
+void RenderManager::DisableScissorRegion()
+{
+	SetScissorRegion(Rectanglei::MakeInvalid());
+}
+
+void RenderManager::SetScissorRegion(Rectanglei new_region)
+{
+	const bool old_scissor_enable = state.scissor_region.Valid();
+	const bool new_scissor_enable = new_region.Valid();
+
+	if (new_scissor_enable != old_scissor_enable)
+		render_interface->EnableScissorRegion(new_scissor_enable);
+
+	if (new_scissor_enable)
+	{
+		new_region.Intersect(Rectanglei::FromSize(viewport_dimensions));
+
+		if (new_region != state.scissor_region)
+			render_interface->SetScissorRegion(new_region.Left(), new_region.Top(), new_region.Width(), new_region.Height());
+	}
+
+	state.scissor_region = new_region;
+}
+
+void RenderManager::DisableClipMask()
+{
+	if (!state.clip_mask_list.empty())
+	{
+		state.clip_mask_list.clear();
+		ApplyClipMask(state.clip_mask_list);
+	}
+}
+
+void RenderManager::SetClipMask(ClipMaskOperation operation, Geometry* geometry, Vector2f translation)
+{
+	RMLUI_ASSERT(geometry);
+	state.clip_mask_list = {ClipMaskGeometry{operation, geometry, translation, nullptr}};
+	ApplyClipMask(state.clip_mask_list);
+}
+
+void RenderManager::SetClipMask(ClipMaskGeometryList in_clip_elements)
+{
+	if (state.clip_mask_list != in_clip_elements)
+	{
+		state.clip_mask_list = std::move(in_clip_elements);
+		ApplyClipMask(state.clip_mask_list);
+	}
+}
+
+void RenderManager::SetTransform(const Matrix4f* p_new_transform)
+{
+	static const Matrix4f identity_transform = Matrix4f::Identity();
+	const Matrix4f& new_transform = (p_new_transform ? *p_new_transform : identity_transform);
+
+	if (state.transform != new_transform)
+	{
+		render_interface->SetTransform(p_new_transform);
+		state.transform = new_transform;
+	}
+}
+
+void RenderManager::ApplyClipMask(const ClipMaskGeometryList& clip_elements)
+{
+	const bool clip_mask_enabled = !clip_elements.empty();
+	render_interface->EnableClipMask(clip_mask_enabled);
+
+	if (clip_mask_enabled)
+	{
+		const Matrix4f initial_transform = state.transform;
+
+		for (const ClipMaskGeometry& element_clip : clip_elements)
+		{
+			SetTransform(element_clip.transform);
+			element_clip.geometry->RenderToClipMask(element_clip.operation, element_clip.absolute_offset);
+		}
+
+		// Apply the initially set transform in case it was changed.
+		SetTransform(&initial_transform);
+	}
+}
+
+void RenderManager::SetState(const RenderState& next)
+{
+	SetScissorRegion(next.scissor_region);
+
+	SetClipMask(next.clip_mask_list);
+
+	SetTransform(&next.transform);
+}
+
+void RenderManager::ResetState()
+{
+	SetState(RenderState{});
+}
+
+} // namespace Rml

+ 71 - 0
Tests/Data/VisualTests/clip_mask.rml

@@ -0,0 +1,71 @@
+<rml>
+<head>
+	<title>Clip mask</title>
+	<link type="text/rcss" href="../style.rcss"/>
+	<meta name="Description" content="Clipping with border-radius should clip along its curved, inner border. Requires clip mask support in the renderer, commonly implemented using a stencil buffer." />
+	<style>
+		.clip {
+			overflow: hidden;
+		}
+		handle {
+			top: auto;
+			right: auto;
+			bottom: auto;
+			left: auto;
+			width: auto;
+			height: auto;
+			display: block;
+			position: relative;
+		}
+		.box {
+			margin: 20dp auto;
+			position: relative;
+			width: 250dp;
+			border-color: #55f;
+			border-width: 20dp;
+			border-radius: 150dp;
+		}
+		.A {
+			height: 70dp;
+			background: #ffd34f;
+			border: 5dp #f57;
+		}
+		.B {
+			left: 50%;
+			background: #ffd34f;
+			border: 5dp #f57;
+			border-radius: 25dp;
+		}
+		.C {
+			width: 50%;
+			top: 50dp;
+			margin: 10dp auto;
+			height: 30dp;
+			background: #d3ff4f;
+			border: 5dp #3a4;
+			border-radius: 50dp;
+		}
+	</style>
+</head>
+
+<body>
+<div class="box clip">
+	Duck aloft
+	<handle move_target="#self" class="A">
+			A
+	</handle>
+	X
+</div>
+<div class="box clip">
+	Y
+	<handle move_target="#self" class="B clip">
+			C
+		<handle move_target="#self" class="C clip">
+				Some long text
+		</handle>
+			Z
+	</handle>
+	W
+</div>
+</body>
+</rml>

+ 2 - 0
Tests/Data/VisualTests/position_absolute_transform.rml

@@ -5,6 +5,7 @@
 	<link rel="help" href="https://www.w3.org/TR/css-transforms-1/#containing-block-for-all-descendants" />
 	<meta name="Description" content="Elements with a local transform or perspective should act as the containing block for absolutely positioned children. "/>
 	<meta name="Assert" content="The child boxes should fit nicely at the bottom right corner of their parent elements."/>
+	<meta name="Assert" content="The child boxes should clip correctly to their containers, when they are scrolled down."/>
 	<style>
 		.container {
 			margin: 15dp 0;
@@ -12,6 +13,7 @@
 			background-color: #ddd;
 			border: 3px #888;
 			overflow: auto;
+			transform: translateY(50%);
 		}
 		.tall { height: 1000px; }
 		.transform {