View Issue Details

IDProjectCategoryView StatusLast Update
0001559OpenMPTGeneralpublic2025-10-25 18:52
ReporterSaga Musix Assigned Tomanx  
PrioritynormalSeverityminorReproducibilityN/A
Status acknowledgedResolutionopen 
Target VersionOpenMPT 1.33 / libopenmpt 0.9 (goals) 
Summary0001559: Don't use WinAPI functions for INI reading/writing
Description

Currently OpenMPT uses ReadPrivateProfile* / WritePrivateProfile*functions provided by Windows for reading and writing its settings files. This has several issues

  • It's relatively slow, which is why we only write setting that we know have changed
  • It requires hacks to enforce writing unicode files

As a consequence of the first point, it's possible that one OpenMPT instance takes a long time to shut down, and a newly launched instance may not see the settings as saved by the previous instance. One particularly worrying example is the following scenario:

  1. Fresh OpenMPT installation, user sees the Welcome dialog and unchecks automatic update checks
  2. User closes OpenMPT. This is the first time the INI file is written, so it takes quite a while to write out all settings.
  3. User immediately reopens OpenMPT. It may now try to search for updates because the old instance might not have finished writing the settings yet.

A custom INI implementation should avoid this scenario. While the file is being written, other OpenMPT instances should wait until the file is no longer locked, rather than reading incomplete settings.

TagsNo tags attached.
Has the bug occurred in previous versions?
Tested code revision (in case you know it)

Activities

manx

manx

2025-08-16 16:27

administrator   ~0006447

A custom INI implementation should avoid this scenario. While the file is being written, other OpenMPT instances should wait until the file is no longer locked, rather than reading incomplete settings.

I do not think this would be necessary. Atomically writing the file is the easier approach, because we do not (and do not want to, and cannot) guarantee settings consistency across different instances running concurrently anyway. Writing the file in 1 go should also be plenty fast enough to avoid the mentioned race condition. If the second instance is started while the first one is still actively running, there is not much we can do anyway, except for maybe flushing the configuration file after the welcome dialog (which would probably be a good idea even with the old implementation).

There are a couple of open questions regarding our own implementation:

  1. Do we want to handle binary data in string data and key names properly? Windows INI parsers do random confusing and undocumented things here, mostly stripping unwanted characters and trimming the result.
  2. Do we want to preserve ordering of the INI file as read from disk, or do we want to canonicalize/sort what we write out?
  3. Should the format be strictly backward compatible with the Windows INI parser, mimicing all its undocumented quirks?
  4. How to handle duplicate entries? (I did not look into how the Windows INI parser handles this)

The INI implementation that I have in an older code base answers these questions as:

  1. escape both key and value data
  2. strictly sorted output, preserving comments to the respective entry on a best effort basis
  3. no, because of 1 and 2
  4. I have not looked into that in detail yet.

In case the answer to 3 is "no", we should use a different file name in order to not break downgrades completely. "OpenMPT.ini" would make sense here.

Saga Musix

Saga Musix

2025-08-30 13:00

administrator   ~0006454

While strict backwards compatibility would be nice to have, I think it should not delay this effort. I think under normal circumstances 3 should not matter at all, only when a user hand-edited the file. As we lost the ability to automatically back up the current OpenMPT configuration during upgrades, using a new settings name file would probably the safest choice.

manx

manx

2025-10-18 17:35

administrator   ~0006514

[Imp] Settings: Write settings in batches of whole sections with WritePrivateProfileSection. This significantly speeds up writing initial settings on the first run from ~50ms to ~5ms on an NVMe SSD.
[Reg] Settings: Comments in INI files are now always deleted. This is a limitation of GetPrivateProfileSection/WritePrivateProfileSection.
[Reg] Settings: Settings inside each section are now ordered alphabetically in the INI file instead of retaining previous ordering. This could be changed.
[Mod] Settings: Always convert all INI files to Unicode when on Windows XP or later and using a UNICODE build. We cannot sensible do this lazily any more, because that would imply testing each individual setting for roundtrip invariance, which would invalidate the whole speedup.

This mainly happened by accident. Using GetPrivateProfileSection would be required anyway for converting from the existing Win32 INI functions to a custom implementation, because we cannot rely on all settings being read eagerly into out internal data structures (i.e. currently not selected Sound Devices). Adding the writing was only a small extra step.

There is another potential problem that I noticed while working on this: We are passing the INI file name to MFC via m_pszProfileName. If we would implement our own incompatible INI format, this would require passing a separate file to MFC. I am not sure what MFC is currently using this file for.

speedup-ini-settings-v1.patch (7,831 bytes)   
Index: mptrack/Settings.cpp
===================================================================
--- mptrack/Settings.cpp	(revision 24333)
+++ mptrack/Settings.cpp	(working copy)
@@ -140,6 +140,16 @@
 	backend->RemoveSection(section);
 }
 
+bool SettingsContainer::BackendsCanWriteMultipleSettings() const
+{
+	return backend->CanWriteMultipleSettings();
+}
+
+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,20 @@
 {
 	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()));
+				value.Clean();
+			}
+		}
+		BackendsWriteMultipleSettings(settings);
+		return;
+	}
 	for(auto &[path, value] : map)
 	{
 		if(value.IsDirty())
@@ -407,7 +431,12 @@
 IniFileSettingsBackend::IniFileSettingsBackend(const mpt::PathString &filename)
 	: filename(filename)
 {
-	return;
+#if defined(UNICODE)
+	if(mpt::osinfo::windows::Version::Current().IsAtLeast(mpt::osinfo::windows::Version::WinXP))
+	{
+		ConvertToUnicode();
+	}
+#endif
 }
 
 IniFileSettingsBackend::~IniFileSettingsBackend()
@@ -493,10 +522,143 @@
 	RemoveSectionRaw(section);
 }
 
+std::set<mpt::winstring> IniFileSettingsBackend::ReadSections() const
+{
+	std::set<mpt::winstring> result;
+	const std::vector<TCHAR> sectionsstr = [&]()
+		{
+			std::vector<TCHAR> buf;
+			buf.resize(1024);
+			while(true)
+			{
+				DWORD bufused = ::GetPrivateProfileSectionNames(buf.data(), mpt::saturate_cast<DWORD>(buf.size()), filename.AsNative().c_str());
+				if(bufused >= (buf.size() - 2))
+				{
+					buf.resize(mpt::exponential_grow(buf.size()));
+					continue;
+				}
+				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, mpt::winstring> IniFileSettingsBackend::ReadSection(const mpt::winstring &section) const
+{
+	std::map<mpt::winstring, mpt::winstring> result;
+	const std::vector<TCHAR> keyvalsstr = [&]()
+		{
+			std::vector<TCHAR> buf;
+			buf.resize(1024);
+			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))
+				{
+					buf.resize(mpt::exponential_grow(buf.size()));
+					continue;
+				}
+				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), keyval.substr(equalpos + 1)));
+	}
+	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::ustring();
+			break;
+	}
+}
 
+void IniFileSettingsBackend::WriteSection(const mpt::winstring &section, const std::map<mpt::winstring, mpt::winstring> keyvalues)
+{
+	mpt::winstring keyvals;
+	for(const auto &[key, val] : keyvalues)
+	{
+		keyvals.append(key);
+		keyvals.append(_T("="));
+		keyvals.append(val);
+		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)
+{
+	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)
+	{
+		std::map<mpt::winstring, mpt::winstring> workingsectionsettings = ReadSection(mpt::ToWin(section));
+		for(const auto &[path, value] : sectionsettings)
+		{
+			workingsectionsettings[mpt::ToWin(path.GetRefKey())] = FormatValueAsIni(value);
+		}
+		WriteSection(mpt::ToWin(section), workingsectionsettings);
+	}
+}
+
+bool IniFileSettingsBackend::CanWriteMultipleSettings() const
+{
+	return true;
+}
+
+
+
 IniFileSettingsContainer::IniFileSettingsContainer(const mpt::PathString &filename)
 	: IniFileSettingsBackend(filename)
 	, SettingsContainer(this)
Index: mptrack/Settings.h
===================================================================
--- mptrack/Settings.h	(revision 24333)
+++ 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 CanWriteMultipleSettings() 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;
@@ -711,6 +715,14 @@
 	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;
+private:
+	std::set<mpt::winstring> ReadSections() const;
+	std::map<mpt::winstring, mpt::winstring> ReadSection(const mpt::winstring &section) const;
+	static mpt::winstring FormatValueAsIni(const SettingValue &value);
+	void WriteSection(const mpt::winstring &section, const std::map<mpt::winstring, mpt::winstring> keyvalues);
+public:
+	virtual bool CanWriteMultipleSettings() const override;
+	virtual void WriteMultipleSettings(const std::map<SettingPath, SettingValue> &settings) override;
 	const mpt::PathString& GetFilename() const { return filename; }
 };
 
speedup-ini-settings-v1.patch (7,831 bytes)   
Saga Musix

Saga Musix

2025-10-19 18:35

administrator   ~0006515

There is another potential problem that I noticed while working on this: We are passing the INI file name to MFC via m_pszProfileName. If we would implement our own incompatible INI format, this would require passing a separate file to MFC. I am not sure what MFC is currently using this file for.

This is for storing the position and visibility of toolbars. It would probably be possible to serialize them outselves by overwriting some functions, then MFC would not be involved at all anymore.

manx

manx

2025-10-25 18:52

administrator   ~0006517

speedup-ini-settings-v3-wip.patch (17,925 bytes)   
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
 
 }

Issue History

Date Modified Username Field Change
2022-01-30 17:48 Saga Musix New Issue
2022-01-30 17:51 Saga Musix Target Version => OpenMPT 1.31.01.00 / libopenmpt 0.7.0 (upgrade first)
2022-10-22 13:45 manx Assigned To => manx
2022-10-22 13:45 manx Status new => acknowledged
2023-04-10 08:24 manx Target Version OpenMPT 1.31.01.00 / libopenmpt 0.7.0 (upgrade first) => OpenMPT 1.32.01.00 / libopenmpt 0.8.0 (upgrade first)
2024-10-26 18:05 manx Target Version OpenMPT 1.32.01.00 / libopenmpt 0.8.0 (upgrade first) => OpenMPT 1.33 / libopenmpt 0.9 (goals)
2025-08-16 16:27 manx Note Added: 0006447
2025-08-30 13:00 Saga Musix Note Added: 0006454
2025-10-18 17:35 manx Note Added: 0006514
2025-10-18 17:35 manx File Added: speedup-ini-settings-v1.patch
2025-10-19 18:35 Saga Musix Note Added: 0006515
2025-10-25 18:52 manx Note Added: 0006517
2025-10-25 18:52 manx File Added: speedup-ini-settings-v3-wip.patch