Index: mptrack/Settings.cpp
===================================================================
--- mptrack/Settings.cpp	(revision 24343)
+++ mptrack/Settings.cpp	(working copy)
@@ -140,6 +140,16 @@
 	backend->RemoveSection(section);
 }
 
+bool SettingsContainer::BackendsCanWriteMultipleSettings() const
+{
+	return backend->CanBatchedSettings();
+}
+
+void SettingsContainer::BackendsWriteMultipleSettings(const std::map<SettingPath, SettingValue> &settings)
+{
+	backend->WriteMultipleSettings(settings);
+}
+
 SettingValue SettingsContainer::ReadSetting(const SettingPath &path, const SettingValue &def) const
 {
 	ASSERT(theApp.InGuiThread());
@@ -242,6 +252,26 @@
 {
 	ASSERT(theApp.InGuiThread());
 	ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler.
+	if(BackendsCanWriteMultipleSettings())
+	{
+		std::map<SettingPath, SettingValue> settings;
+		for(auto &[path, value] : map)
+		{
+			if(value.IsDirty())
+			{
+				settings.insert(std::make_pair(path, value.GetRefValue()));
+			}
+		}
+		BackendsWriteMultipleSettings(settings);
+		for(auto &[path, value] : map)
+		{
+			if(value.IsDirty())
+			{
+				value.Clean();
+			}
+		}
+		return;
+	}
 	for(auto &[path, value] : map)
 	{
 		if(value.IsDirty())
@@ -405,11 +435,22 @@
 
 
 IniFileSettingsBackend::IniFileSettingsBackend(const mpt::PathString &filename)
-	: filename(filename)
+	: IniFileSettingsBackend(filename, CachePolicy::Cached)
 {
 	return;
 }
 
+IniFileSettingsBackend::IniFileSettingsBackend(const mpt::PathString &filename, CachePolicy cachePolicy)
+	: filename(filename)
+{
+	OPENMPT_PROFILE_FUNCTION(Profiler::Settings);
+	if(cachePolicy == CachePolicy::Cached)
+	{
+		ConvertToUnicode();
+		cache = ReadAllSectionsRaw();
+	}
+}
+
 IniFileSettingsBackend::~IniFileSettingsBackend()
 {
 	return;
@@ -469,14 +510,41 @@
 SettingValue IniFileSettingsBackend::ReadSetting(const SettingPath &path, const SettingValue &def) const
 {
 	OPENMPT_PROFILE_FUNCTION(Profiler::Settings);
-	switch(def.GetType())
+	if(cache.has_value())
 	{
-	case SettingTypeBool: return SettingValue(ReadSettingRaw(path, def.as<bool>()), def.GetTypeTag()); break;
-	case SettingTypeInt: return SettingValue(ReadSettingRaw(path, def.as<int32>()), def.GetTypeTag()); break;
-	case SettingTypeFloat: return SettingValue(ReadSettingRaw(path, def.as<double>()), def.GetTypeTag()); break;
-	case SettingTypeString: return SettingValue(ReadSettingRaw(path, def.as<mpt::ustring>()), def.GetTypeTag()); break;
-	case SettingTypeBinary: return SettingValue(ReadSettingRaw(path, def.as<std::vector<std::byte> >()), def.GetTypeTag()); break;
-	default: return SettingValue(); break;
+		const auto sectionit = (*cache).find(mpt::ToWin(path.GetRefSection()));
+		if(sectionit != (*cache).end())
+		{
+			const std::map<mpt::winstring, std::optional<mpt::winstring>> &section = sectionit->second;
+			const auto it = section.find(mpt::ToWin(path.GetRefKey()));
+			if(it != section.end())
+			{
+				if(it->second.has_value())
+				{
+					return ParseValueFromIni(it->second.value(), def);
+				} else
+				{
+					return def;
+				}
+			} else
+			{
+				return def;
+			}
+		} else
+		{
+			return def;
+		}
+	} else
+	{
+		switch(def.GetType())
+		{
+		case SettingTypeBool: return SettingValue(ReadSettingRaw(path, def.as<bool>()), def.GetTypeTag()); break;
+		case SettingTypeInt: return SettingValue(ReadSettingRaw(path, def.as<int32>()), def.GetTypeTag()); break;
+		case SettingTypeFloat: return SettingValue(ReadSettingRaw(path, def.as<double>()), def.GetTypeTag()); break;
+		case SettingTypeString: return SettingValue(ReadSettingRaw(path, def.as<mpt::ustring>()), def.GetTypeTag()); break;
+		case SettingTypeBinary: return SettingValue(ReadSettingRaw(path, def.as<std::vector<std::byte> >()), def.GetTypeTag()); break;
+		default: return SettingValue(); break;
+		}
 	}
 }
 
@@ -493,12 +561,22 @@
 	case SettingTypeBinary: WriteSettingRaw(path, val.as<std::vector<std::byte> >()); break;
 	default: break;
 	}
+	if(cache.has_value())
+	{
+		(*cache)[mpt::ToWin(path.GetRefSection())][mpt::ToWin(path.GetRefKey())] = FormatValueAsIni(val);
+	}
 }
 
 void IniFileSettingsBackend::RemoveSetting(const SettingPath &path)
 {
 	OPENMPT_PROFILE_FUNCTION(Profiler::Settings);
-	RemoveSettingRaw(path);
+	if(cache.has_value())
+	{
+		(*cache)[mpt::ToWin(path.GetRefSection())][mpt::ToWin(path.GetRefKey())] = std::nullopt;
+	} else
+	{
+		RemoveSettingRaw(path);
+	}
 }
 
 void IniFileSettingsBackend::RemoveSection(const mpt::ustring &section)
@@ -505,19 +583,255 @@
 {
 	OPENMPT_PROFILE_FUNCTION(Profiler::Settings);
 	RemoveSectionRaw(section);
+	if(cache.has_value())
+	{
+		const auto it = (*cache).find(mpt::ToWin(section));
+		if(it != (*cache).end())
+		{
+			(*cache).erase(it);
+		}
+	}
 }
 
+std::set<mpt::winstring> IniFileSettingsBackend::ReadSectionNamesRaw() const
+{
+	std::set<mpt::winstring> result;
+	const std::vector<TCHAR> sectionsstr = [&]()
+		{
+			std::vector<TCHAR> buf;
+			buf.resize(mpt::IO::BUFFERSIZE_SMALL);
+			while(true)
+			{
+				DWORD bufused = ::GetPrivateProfileSectionNames(buf.data(), mpt::saturate_cast<DWORD>(buf.size()), filename.AsNative().c_str());
+				if(bufused >= (buf.size() - 2))
+				{
+					std::size_t newsize = mpt::exponential_grow(buf.size());
+					buf.resize(0);
+					buf.resize(newsize);
+					continue;
+				}
+				if(bufused >= 1)
+				{
+					bufused -= 1;  // terminating \0
+				}
+				buf.resize(bufused);
+				break;
+			};
+			return buf;
+		}();
+	const std::vector<mpt::winstring> sections = mpt::split(mpt::winstring(sectionsstr.data(), sectionsstr.size()), mpt::winstring(_T("\0"), 1));
+	for(const auto &section : sections)
+	{
+		result.insert(section);
+	}
+	return result;
+}
 
+std::map<mpt::winstring, std::optional<mpt::winstring>> IniFileSettingsBackend::ReadNamedSectionRaw(const mpt::winstring &section) const
+{
+	std::map<mpt::winstring, std::optional<mpt::winstring>> result;
+	const std::vector<TCHAR> keyvalsstr = [&]()
+		{
+			std::vector<TCHAR> buf;
+			buf.resize(mpt::IO::BUFFERSIZE_SMALL);
+			while(true)
+			{
+				DWORD bufused = ::GetPrivateProfileSection(mpt::ToWin(section).c_str(), buf.data(), mpt::saturate_cast<DWORD>(buf.size()), filename.AsNative().c_str());
+				if(bufused >= (buf.size() - 2))
+				{
+					std::size_t newsize = mpt::exponential_grow(buf.size());
+					buf.resize(0);
+					buf.resize(newsize);
+					continue;
+				}
+				if(bufused >= 1)
+				{
+					bufused -= 1;  // terminating \0
+				}
+				buf.resize(bufused);
+				break;
+			};
+			return buf;
+		}();
+	const std::vector<mpt::winstring> keyvals = mpt::split(mpt::winstring(keyvalsstr.data(), keyvalsstr.size()), mpt::winstring(_T("\0"), 1));
+	for(const auto &keyval : keyvals)
+	{
+		const auto equalpos = keyval.find(_T("="));
+		if(equalpos == mpt::winstring::npos)
+		{
+			continue;
+		}
+		if(equalpos == 0)
+		{
+			continue;
+		}
+		result.insert(std::make_pair(keyval.substr(0, equalpos), std::make_optional(keyval.substr(equalpos + 1))));
+	}
+	return result;
+}
 
+std::map<mpt::winstring, std::map<mpt::winstring, std::optional<mpt::winstring>>> IniFileSettingsBackend::ReadAllSectionsRaw() const
+{
+	std::map<mpt::winstring, std::map<mpt::winstring, std::optional<mpt::winstring>>> result;
+	const std::set<mpt::winstring> sectionnames = ReadSectionNamesRaw();
+	for(const mpt::winstring &sectionname : sectionnames)
+	{
+		result.insert(std::make_pair(sectionname, ReadNamedSectionRaw(sectionname)));
+	}
+	return result;
+}
 
+mpt::winstring IniFileSettingsBackend::FormatValueAsIni(const SettingValue &value)
+{
+	switch(value.GetType())
+	{
+		case SettingTypeBool:
+			return mpt::tfmt::val(value.as<bool>());
+			break;
+		case SettingTypeInt:
+			return mpt::tfmt::val(value.as<int32>());
+			break;
+		case SettingTypeFloat:
+			return mpt::tfmt::val(value.as<double>());
+			break;
+		case SettingTypeString:
+			return mpt::ToWin(value.as<mpt::ustring>());
+			break;
+		case SettingTypeBinary:
+			{
+				std::vector<std::byte> data = value.as<std::vector<std::byte>>();
+				uint8 checksum = 0;
+				for(const std::byte x : data) {
+					checksum += mpt::byte_cast<uint8>(x);
+				}
+				return mpt::ToWin(mpt::encode_hex(mpt::as_span(data)) + mpt::ufmt::HEX0<2>(checksum));
+			}
+			break;
+		case SettingTypeNone:
+		default:
+			return mpt::winstring();
+			break;
+	}
+}
 
+
+SettingValue IniFileSettingsBackend::ParseValueFromIni(const mpt::winstring &str, const SettingValue &def)
+{
+	switch(def.GetType())
+	{
+		case SettingTypeBool:
+			return SettingValue(mpt::parse_or<bool>(mpt::trim(str), def.as<bool>()), def.GetTypeTag());
+			break;
+		case SettingTypeInt:
+			return SettingValue(mpt::parse_or<int32>(mpt::trim(str), def.as<int32>()), def.GetTypeTag());
+			break;
+		case SettingTypeFloat:
+			return SettingValue(mpt::parse_or<double>(mpt::trim(str), def.as<double>()), def.GetTypeTag());
+			break;
+		case SettingTypeString:
+			return SettingValue(mpt::ToUnicode(str), def.GetTypeTag());
+			break;
+		case SettingTypeBinary:
+			{
+				mpt::ustring ustr = mpt::trim(mpt::ToUnicode(str));
+				if((ustr.length() % 2) != 0)
+				{
+					return SettingValue(def.as<std::vector<std::byte>>(), def.GetTypeTag());
+				}
+				std::vector<std::byte> data = mpt::decode_hex(ustr);
+				if(data.size() < 1)
+				{
+					return SettingValue(def.as<std::vector<std::byte>>(), def.GetTypeTag());
+				}
+				const uint8 storedchecksum = mpt::byte_cast<uint8>(data[data.size() - 1]);
+				data.resize(data.size() - 1);
+				uint8 calculatedchecksum = 0;
+				for(const std::byte x : data) {
+					calculatedchecksum += mpt::byte_cast<uint8>(x);
+				}
+				if(calculatedchecksum != storedchecksum)
+				{
+					return SettingValue(def.as<std::vector<std::byte>>(), def.GetTypeTag());
+				}
+				return SettingValue(data, def.GetTypeTag());
+			}
+			break;
+		default:
+			return SettingValue();
+			break;
+	}
+}
+
+void IniFileSettingsBackend::WriteSectionRaw(const mpt::winstring &section, const std::map<mpt::winstring, std::optional<mpt::winstring>> &keyvalues)
+{
+	mpt::winstring keyvals;
+	for(const auto &[key, val] : keyvalues)
+	{
+		if(val.has_value())
+		{
+			keyvals.append(key);
+			keyvals.append(_T("="));
+			keyvals.append(val.value());
+			keyvals.append(mpt::winstring(_T("\0"), 1));
+		}
+	}
+	keyvals.append(mpt::winstring(_T("\0"), 1));
+	::WritePrivateProfileSection(section.c_str(), keyvals.c_str(), filename.AsNative().c_str());
+}
+
+void IniFileSettingsBackend::WriteMultipleSettings(const std::map<SettingPath, SettingValue> &settings)
+{
+	OPENMPT_PROFILE_FUNCTION(Profiler::Settings);
+	std::map<mpt::ustring, std::map<SettingPath, SettingValue>> sectionssettings;
+	for(const auto &[path, value] : settings)
+	{
+		sectionssettings[path.GetRefSection()][path] = value;
+	}
+	for(const auto &[section, sectionsettings] : sectionssettings)
+	{
+		// we need to re-read the section before writing it out in order to not overwrite settings written by MFC
+		std::map<mpt::winstring, std::optional<mpt::winstring>> workingsectionsettings = ReadNamedSectionRaw(mpt::ToWin(section));
+		// apply deleted settings from cache
+		for(const auto &[key, value] : (*cache)[mpt::ToWin(section)])
+		{
+			if(!value.has_value())
+			{
+				workingsectionsettings[key] = std::nullopt;
+			}
+		}
+		for(const auto &[path, value] : sectionsettings)
+		{
+			workingsectionsettings[mpt::ToWin(path.GetRefKey())] = FormatValueAsIni(value);
+		}
+		WriteSectionRaw(mpt::ToWin(section), workingsectionsettings);
+		if(cache.has_value())
+		{
+			(*cache)[mpt::ToWin(section)] = std::move(workingsectionsettings);
+		}
+	}
+}
+
+bool IniFileSettingsBackend::CanBatchedSettings() const
+{
+	return cache.has_value();
+}
+
+
+
 IniFileSettingsContainer::IniFileSettingsContainer(const mpt::PathString &filename)
-	: IniFileSettingsBackend(filename)
+	: IniFileSettingsBackend(filename, CachePolicy::Cached)
 	, SettingsContainer(this)
 {
 	return;
 }
 
+IniFileSettingsContainer::IniFileSettingsContainer(const mpt::PathString &filename, CachePolicy cachePolicy)
+	: IniFileSettingsBackend(filename, cachePolicy)
+	, SettingsContainer(this)
+{
+	return;
+}
+
 IniFileSettingsContainer::~IniFileSettingsContainer()
 {
 	return;
Index: mptrack/Settings.h
===================================================================
--- mptrack/Settings.h	(revision 24343)
+++ mptrack/Settings.h	(working copy)
@@ -408,6 +408,8 @@
 	virtual void WriteSetting(const SettingPath &path, const SettingValue &val) = 0;
 	virtual void RemoveSetting(const SettingPath &path) = 0;
 	virtual void RemoveSection(const mpt::ustring &section) = 0;
+	virtual bool CanBatchedSettings() const = 0;
+	virtual void WriteMultipleSettings(const std::map<SettingPath, SettingValue> &settings) = 0;
 protected:
 	virtual ~ISettingsBackend() = default;
 };
@@ -448,6 +450,8 @@
 	void BackendsWriteSetting(const SettingPath &path, const SettingValue &val);
 	void BackendsRemoveSetting(const SettingPath &path);
 	void BackendsRemoveSection(const mpt::ustring &section);
+	bool BackendsCanWriteMultipleSettings() const;
+	void BackendsWriteMultipleSettings(const std::map<SettingPath, SettingValue> &settings);
 	void NotifyListeners(const SettingPath &path);
 	SettingValue ReadSetting(const SettingPath &path, const SettingValue &def) const;
 	bool IsDefaultSetting(const SettingPath &path) const;
@@ -684,11 +688,26 @@
 };
 
 
+enum class CachePolicy
+{
+	Direct,
+	Cached,
+};
+
 class IniFileSettingsBackend : public ISettingsBackend
 {
+public:
 private:
 	const mpt::PathString filename;
+	std::optional<std::map<mpt::winstring, std::map<mpt::winstring, std::optional<mpt::winstring>>>> cache;
+#ifdef ENABLE_TESTS
+public:
+#else
 private:
+#endif
+	std::set<mpt::winstring> ReadSectionNamesRaw() const;
+	std::map<mpt::winstring, std::optional<mpt::winstring>> ReadNamedSectionRaw(const mpt::winstring &section) const;
+	std::map<mpt::winstring, std::map<mpt::winstring, std::optional<mpt::winstring>>> ReadAllSectionsRaw() const;
 	std::vector<std::byte> ReadSettingRaw(const SettingPath &path, const std::vector<std::byte> &def) const;
 	mpt::ustring ReadSettingRaw(const SettingPath &path, const mpt::ustring &def) const;
 	double ReadSettingRaw(const SettingPath &path, double def) const;
@@ -699,12 +718,17 @@
 	void WriteSettingRaw(const SettingPath &path, double val);
 	void WriteSettingRaw(const SettingPath &path, int32 val);
 	void WriteSettingRaw(const SettingPath &path, bool val);
+	void WriteSectionRaw(const mpt::winstring &section, const std::map<mpt::winstring, std::optional<mpt::winstring>> &keyvalues);
 	void RemoveSettingRaw(const SettingPath &path);
 	void RemoveSectionRaw(const mpt::ustring &section);
+private:
+	static mpt::winstring FormatValueAsIni(const SettingValue &value);
+	static SettingValue ParseValueFromIni(const mpt::winstring &str, const SettingValue &def);
 	static mpt::winstring GetSection(const SettingPath &path);
 	static mpt::winstring GetKey(const SettingPath &path);
 public:
-	IniFileSettingsBackend(const mpt::PathString &filename);
+	[[deprecated]] IniFileSettingsBackend(const mpt::PathString &filename);
+	IniFileSettingsBackend(const mpt::PathString &filename, CachePolicy cachePolicy);
 	~IniFileSettingsBackend() override;
 	void ConvertToUnicode(const mpt::ustring &backupTag = mpt::ustring());
 	virtual SettingValue ReadSetting(const SettingPath &path, const SettingValue &def) const override;
@@ -711,6 +735,9 @@
 	virtual void WriteSetting(const SettingPath &path, const SettingValue &val) override;
 	virtual void RemoveSetting(const SettingPath &path) override;
 	virtual void RemoveSection(const mpt::ustring &section) override;
+	virtual bool CanBatchedSettings() const override;
+	virtual void WriteMultipleSettings(const std::map<SettingPath, SettingValue> &settings) override;
+public:
 	const mpt::PathString& GetFilename() const { return filename; }
 };
 
@@ -717,7 +744,8 @@
 class IniFileSettingsContainer : private IniFileSettingsBackend, public SettingsContainer
 {
 public:
-	IniFileSettingsContainer(const mpt::PathString &filename);
+	[[deprecated]] IniFileSettingsContainer(const mpt::PathString &filename);
+	IniFileSettingsContainer(const mpt::PathString &filename, CachePolicy cachePolicy);
 	~IniFileSettingsContainer() override;
 };
 
Index: test/test.cpp
===================================================================
--- test/test.cpp	(revision 24343)
+++ test/test.cpp	(working copy)
@@ -2737,9 +2737,35 @@
 			VERIFY_EQUAL(inifile.ReadSetting(SettingPath{U_("S1"), U_("bar1")}, U_("empty")).as<mpt::ustring>(), U_("a"));
 			VERIFY_EQUAL(inifile.ReadSetting(SettingPath{U_("S1"), U_("bar2")}, U_("empty")).as<mpt::ustring>(), U_("empty"));
 		}
+		{
+			IniFileSettingsBackend inifile{filename};
+			const std::set<mpt::winstring> a = inifile.ReadSectionNamesRaw();
+			for(const mpt::winstring & s : a)
+			{
+				std::map<mpt::winstring, std::optional<mpt::winstring>> b = inifile.ReadNamedSectionRaw(s);
+			}
+		}
 		DeleteFile(mpt::support_long_path(filename.AsNative()).c_str());
 	}
 
+	{
+		const mpt::PathString filename = theApp.GetConfigPath() + P_("test.ini");
+		std::vector<std::byte> data;
+		for(std::size_t i = 0; i < 10; ++i)
+		{
+			data.push_back(mpt::byte_cast<std::byte>(static_cast<uint8>(i)));
+		}
+		{
+			IniFileSettingsBackend inifile{filename, CachePolicy::Cached};
+			inifile.WriteMultipleSettings({{SettingPath(U_("Test"), U_("Data")), data}});
+		}
+		{
+			IniFileSettingsBackend inifile{filename, CachePolicy::Cached};
+			VERIFY_EQUAL(inifile.ReadSetting(SettingPath(U_("Test"), U_("Data")), SettingValue(data)).as<std::vector<std::byte>>(), data);
+		}
+		DeleteFile(mpt::support_long_path(filename.AsNative()).c_str());
+	}
+
 #endif // MODPLUG_TRACKER
 
 }
