Browse Source

Defer texture loading until it is visible

- Try to load a texture only once in case of a failed load, or until release is called on it.
- Add unit tests for the expected behavior.
Michael Ragazzon 10 months ago
parent
commit
3b96321d4b

+ 1 - 1
Source/Core/RenderManager.cpp

@@ -104,7 +104,7 @@ Texture RenderManager::LoadTexture(const String& source, const String& document_
 	else
 		GetSystemInterface()->JoinPath(path, StringUtilities::Replace(document_path, '|', ':'), source);
 
-	return Texture(this, texture_database->file_database.LoadTexture(render_interface, path));
+	return Texture(this, texture_database->file_database.InsertTexture(path));
 }
 
 CallbackTexture RenderManager::MakeCallbackTexture(CallbackTextureFunction callback)

+ 12 - 12
Source/Core/TextureDatabase.cpp

@@ -116,22 +116,16 @@ FileTextureDatabase::~FileTextureDatabase()
 #endif
 }
 
-TextureFileIndex FileTextureDatabase::LoadTexture(RenderInterface* render_interface, const String& source)
+TextureFileIndex FileTextureDatabase::InsertTexture(const String& source)
 {
 	auto it = texture_map.find(source);
 	if (it != texture_map.end())
 		return it->second;
 
-	FileTextureEntry entry = LoadTextureEntry(render_interface, source);
-	if (!entry.texture_handle)
-	{
-		Rml::Log::Message(Rml::Log::LT_WARNING, "Could not load texture: %s", source.c_str());
-		return TextureFileIndex::Invalid;
-	}
-
+	// The texture is not yet loaded from the render interface. That is deferred until the texture is needed, such as when it becomes visible.
 	const auto index = TextureFileIndex(texture_list.size());
 	texture_map[source] = index;
-	texture_list.push_back(std::move(entry));
+	texture_list.push_back({});
 
 	return index;
 }
@@ -140,6 +134,11 @@ FileTextureDatabase::FileTextureEntry FileTextureDatabase::LoadTextureEntry(Rend
 {
 	FileTextureEntry result = {};
 	result.texture_handle = render_interface->LoadTexture(result.dimensions, source);
+	if (!result.texture_handle)
+	{
+		result.load_texture_failed = true;
+		Rml::Log::Message(Rml::Log::LT_WARNING, "Could not load texture: %s", source.c_str());
+	}
 	return result;
 }
 
@@ -151,7 +150,8 @@ FileTextureDatabase::FileTextureEntry& FileTextureDatabase::EnsureLoaded(RenderI
 		auto it = std::find_if(texture_map.begin(), texture_map.end(), [index](const auto& pair) { return pair.second == index; });
 		RMLUI_ASSERT(it != texture_map.end());
 		const String& source = it->first;
-		entry = LoadTextureEntry(render_interface, source);
+		if (!entry.load_texture_failed)
+			entry = LoadTextureEntry(render_interface, source);
 	}
 	return entry;
 }
@@ -185,7 +185,7 @@ bool FileTextureDatabase::ReleaseTexture(RenderInterface* render_interface, cons
 	if (texture.texture_handle)
 	{
 		render_interface->ReleaseTexture(texture.texture_handle);
-		texture.texture_handle = {};
+		texture = {};
 		return true;
 	}
 
@@ -199,7 +199,7 @@ void FileTextureDatabase::ReleaseAllTextures(RenderInterface* render_interface)
 		if (texture.texture_handle)
 		{
 			render_interface->ReleaseTexture(texture.texture_handle);
-			texture.texture_handle = {};
+			texture = {};
 		}
 	}
 }

+ 2 - 1
Source/Core/TextureDatabase.h

@@ -69,7 +69,7 @@ public:
 	FileTextureDatabase();
 	~FileTextureDatabase();
 
-	TextureFileIndex LoadTexture(RenderInterface* render_interface, const String& source);
+	TextureFileIndex InsertTexture(const String& source);
 
 	TextureHandle GetHandle(RenderInterface* render_interface, TextureFileIndex index);
 	Vector2i GetDimensions(RenderInterface* render_interface, TextureFileIndex index);
@@ -83,6 +83,7 @@ private:
 	struct FileTextureEntry {
 		TextureHandle texture_handle = {};
 		Vector2i dimensions;
+		bool load_texture_failed = false;
 	};
 
 	FileTextureEntry LoadTextureEntry(RenderInterface* render_interface, const String& source);

+ 4 - 1
Tests/Source/Common/TestsInterface.cpp

@@ -155,9 +155,12 @@ void TestsRenderInterface::RenderToClipMask(Rml::ClipMaskOperation /*mask_operat
 	counters.render_to_clip_mask += 1;
 }
 
-Rml::TextureHandle TestsRenderInterface::LoadTexture(Rml::Vector2i& texture_dimensions, const Rml::String& /*source*/)
+Rml::TextureHandle TestsRenderInterface::LoadTexture(Rml::Vector2i& texture_dimensions, const Rml::String& source)
 {
 	counters.load_texture += 1;
+	if (source.find("invalid") != Rml::String::npos)
+		return 0;
+
 	texture_dimensions.x = 512;
 	texture_dimensions.y = 256;
 	return 1;

+ 91 - 0
Tests/Source/UnitTests/Core.cpp

@@ -49,6 +49,7 @@ static const String document_textures_rml = R"(
 			top: 0;
 			right: 0;
 			bottom: 0;
+			font-family: LatoLatin;
 		}
 		div.file {
 			height: 100px;
@@ -78,6 +79,7 @@ static const String document_textures_rml = R"(
 	<div class="file"/>
 	<div class="sprite"/>
 	<progress direction="clockwise" start-edge="bottom" value="0.5"/>
+	abc
 </div>
 </body>
 </rml>
@@ -158,6 +160,95 @@ TEST_CASE("core.texture_source_list")
 	TestsShell::ShutdownShell();
 }
 
+TEST_CASE("core.load_texture_only_when_visible")
+{
+	TestsRenderInterface* render_interface = TestsShell::GetTestsRenderInterface();
+	// This test only works with the dummy renderer.
+	if (!render_interface)
+		return;
+
+	const auto& counters = render_interface->GetCounters();
+	REQUIRE(counters.load_texture == 0);
+	REQUIRE(counters.generate_texture == 0);
+	REQUIRE(counters.release_texture == 0);
+
+	Context* context = TestsShell::GetContext();
+	REQUIRE(context);
+
+	ElementDocument* document = context->LoadDocumentFromMemory(document_textures_rml);
+	Element* child_div = document->GetFirstChild();
+
+	SUBCASE("Invisible")
+	{
+		CHECK(counters.load_texture == 0);
+		CHECK(counters.generate_texture == 0);
+		CHECK(counters.release_texture == 0);
+
+		TestsShell::RenderLoop();
+		CHECK(counters.load_texture == 0);
+		CHECK(counters.generate_texture == 0);
+		CHECK(counters.release_texture == 0);
+
+		document->Show();
+		TestsShell::RenderLoop();
+		CHECK(counters.load_texture == 0);
+		CHECK(counters.generate_texture == 0);
+		CHECK(counters.release_texture == 0);
+	}
+
+	SUBCASE("Visible")
+	{
+		child_div->SetProperty(PropertyId::Display, Style::Display::Block);
+		document->Show();
+		TestsShell::RenderLoop();
+		REQUIRE(counters.load_texture == 4);
+		REQUIRE(counters.generate_texture > 0);
+		REQUIRE(counters.release_texture == 0);
+	}
+
+	SUBCASE("Partially visible")
+	{
+		child_div->SetProperty(PropertyId::Display, Style::Display::Block);
+		ElementList elements;
+		document->GetElementsByTagName(elements, "img");
+		REQUIRE(elements.size() == 1);
+		elements.front()->SetProperty(PropertyId::Display, Style::Display::None);
+
+		document->Show();
+		TestsShell::RenderLoop();
+		REQUIRE(counters.load_texture == 3);
+		REQUIRE(counters.generate_texture > 0);
+		REQUIRE(counters.release_texture == 0);
+	}
+
+	document->Close();
+	TestsShell::ShutdownShell();
+}
+
+TEST_CASE("core.warn_missing_texture_once_when_visible")
+{
+	Context* context = TestsShell::GetContext();
+	REQUIRE(context);
+
+	const String document_missing_textures_rml = StringUtilities::Replace(document_textures_rml, ".tga", "_invalid.tga");
+	ElementDocument* document = context->LoadDocumentFromMemory(document_missing_textures_rml);
+	Element* child_div = document->GetFirstChild();
+
+	TestsShell::SetNumExpectedWarnings(0);
+	document->Show();
+	TestsShell::RenderLoop();
+
+	TestsShell::SetNumExpectedWarnings(4);
+	child_div->SetProperty(PropertyId::Display, Style::Display::Block);
+	TestsShell::RenderLoop();
+
+	TestsShell::SetNumExpectedWarnings(0);
+	TestsShell::RenderLoop();
+
+	document->Close();
+	TestsShell::ShutdownShell();
+}
+
 TEST_CASE("core.release_resources")
 {
 	TestsRenderInterface* render_interface = TestsShell::GetTestsRenderInterface();