Index: common/Profiler.h
===================================================================
--- common/Profiler.h	(revision 24613)
+++ common/Profiler.h	(working copy)
@@ -54,7 +54,7 @@
 
 #if defined(MODPLUG_TRACKER)
 
-//#define USE_PROFILER
+#define USE_PROFILER
 
 #endif
 
Index: mptrack/Mptrack.h
===================================================================
--- mptrack/Mptrack.h	(revision 24613)
+++ mptrack/Mptrack.h	(working copy)
@@ -35,12 +35,13 @@
 class TrackerSettings;
 class ImmediateWindowsIniFileSettingsBackend;
 class BatchedWindowsIniFileSettingsBackend;
+class CachedIniFileSettingsBackend;
 template <typename Backend> class FileSettingsContainer;
 #ifndef IniFileSettingsBackend
-#define IniFileSettingsBackend ImmediateWindowsIniFileSettingsBackend
+#define IniFileSettingsBackend CachedIniFileSettingsBackend
 #endif
 #ifndef IniFileSettingsContainer
-#define IniFileSettingsContainer FileSettingsContainer<ImmediateWindowsIniFileSettingsBackend>
+#define IniFileSettingsContainer FileSettingsContainer<CachedIniFileSettingsBackend>
 #endif
 class SettingsContainer;
 class ComponentManagerSettings;
Index: mptrack/SettingsIni.cpp
===================================================================
--- mptrack/SettingsIni.cpp	(revision 24613)
+++ mptrack/SettingsIni.cpp	(working copy)
@@ -13,10 +13,19 @@
 
 #include "SettingsIni.h"
 
+#include "mpt/base/alloc.hpp"
+#include "mpt/base/bit.hpp"
+#include "mpt/base/integer.hpp"
 #include "mpt/binary/hex.hpp"
+#include "mpt/format/message_macros.hpp"
 #include "mpt/io_file/fstream.hpp"
 #include "mpt/io_file/outputfile.hpp"
+#include "mpt/io_read/filecursor.hpp"
+#include "mpt/io_read/filecursor_memory.hpp"
+#include "mpt/io_read/filereader.hpp"
 #include "mpt/parse/parse.hpp"
+#include "mpt/string/types.hpp"
+#include "mpt/string_transcode/transcode.hpp"
 
 #include "../common/misc_util.h"
 #include "../common/mptFileIO.h"
@@ -23,15 +32,24 @@
 #include "../common/mptStringBuffer.h"
 
 #include <algorithm>
+#include <list>
+#include <map>
+#include <optional>
+#include <set>
+#include <string>
+#include <string_view>
 #include <utility>
+#include <vector>
 
+#include <cstring>
 
 
+
 OPENMPT_NAMESPACE_BEGIN
 
 
 
-WindowsIniFileBase::WindowsIniFileBase(mpt::PathString filename_)
+IniFileBase::IniFileBase(mpt::PathString filename_)
 	: filename(std::move(filename_))
 	, file(filename)
 {
@@ -38,6 +56,24 @@
 	return;
 }
 
+const mpt::PathString &IniFileBase::Filename() const
+{
+	return filename;
+}
+
+mpt::PathString IniFileBase::GetFilename() const
+{
+	return filename;
+}
+
+
+
+WindowsIniFileBase::WindowsIniFileBase(mpt::PathString filename_)
+	: IniFileBase(std::move(filename_))
+{
+	return;
+}
+
 void WindowsIniFileBase::ConvertToUnicode(const mpt::ustring &backupTag)
 {
 	// Force ini file to be encoded in UTF16.
@@ -90,17 +126,248 @@
 #endif
 }
 
-const mpt::PathString &WindowsIniFileBase::Filename() const
+
+
+mpt::ustring IniFileHelpers::DecodeTextWithBOM(mpt::const_byte_span filedata, const mpt::PathString &filename)
 {
-	return filename;
+	IniEncoding encoding = IniEncoding::ANSI;
+	mpt::IO::Offset data_offset = 0;
+	const std::array<std::byte, 4> bom_utf32be = {mpt::byte_cast<std::byte>(uint8{0x00}), mpt::byte_cast<std::byte>(uint8{0x00}), mpt::byte_cast<std::byte>(uint8{0xfe}), mpt::byte_cast<std::byte>(uint8{0xff})};
+	const std::array<std::byte, 4> bom_utf32le = {mpt::byte_cast<std::byte>(uint8{0xff}), mpt::byte_cast<std::byte>(uint8{0xfe}), mpt::byte_cast<std::byte>(uint8{0x00}), mpt::byte_cast<std::byte>(uint8{0x00})};
+	const std::array<std::byte, 2> bom_utf16be = {mpt::byte_cast<std::byte>(uint8{0xfe}), mpt::byte_cast<std::byte>(uint8{0xff})};
+	const std::array<std::byte, 2> bom_utf16le = {mpt::byte_cast<std::byte>(uint8{0xff}), mpt::byte_cast<std::byte>(uint8{0xfe})};
+	const std::array<std::byte, 3> bom_utf8 = {mpt::byte_cast<std::byte>(uint8{0xEF}), mpt::byte_cast<std::byte>(uint8{0xBB}), mpt::byte_cast<std::byte>(uint8{0xBF})};
+	if((filedata.size() >= bom_utf32be.size()) && (std::memcmp(filedata.data(), bom_utf32be.data(), bom_utf32be.size()) == 0))
+	{
+		encoding = IniEncoding::UTF32BE;
+		data_offset = 4;
+	} else if((filedata.size() >= bom_utf32le.size()) && (std::memcmp(filedata.data(), bom_utf32le.data(), bom_utf32le.size()) == 0))
+	{
+		encoding = IniEncoding::UTF32LE;
+		data_offset = 4;
+	} else if((filedata.size() >= bom_utf16be.size()) && (std::memcmp(filedata.data(), bom_utf16be.data(), bom_utf16be.size()) == 0))
+	{
+		encoding = IniEncoding::UTF16BE;
+		data_offset = 2;
+	} else if((filedata.size() >= bom_utf16le.size()) && (std::memcmp(filedata.data(), bom_utf16le.data(), bom_utf16le.size()) == 0))
+	{
+		encoding = IniEncoding::UTF16LE;
+		data_offset = 2;
+	} else if((filedata.size() >= bom_utf8.size()) && (std::memcmp(filedata.data(), bom_utf8.data(), bom_utf8.size()) == 0))
+	{
+		encoding = IniEncoding::UTF8;
+		data_offset = 3;
+	} else
+	{
+		encoding = IniEncoding::ANSI;
+		data_offset = 0;
+	}
+	mpt::const_byte_span textdata = filedata.subspan(data_offset);
+	mpt::ustring filetext;
+	switch(encoding)
+	{
+		case IniEncoding::UTF32BE:
+			{
+				if((textdata.size() % sizeof(char32_t)) != 0)
+				{
+					MPT_LOG_GLOBAL(LogError, "Settings", MPT_UFORMAT_MESSAGE("{}: {}")(filename, U_("UTF32 encoding detected, but file size is not a multiple of 4.")));
+				}
+				MPT_MAYBE_CONSTANT_IF(mpt::endian_is_big())
+				{
+					std::u32string utf32data;
+					std::size_t count = textdata.size() / sizeof(char32_t);
+					utf32data.resize(count);
+					std::memcpy(utf32data.data(), textdata.data(), count * sizeof(char32_t));
+					filetext = mpt::transcode<mpt::ustring>(utf32data);
+				} else
+				{
+					auto fc = mpt::IO::make_FileCursor<mpt::PathString>(textdata);
+					std::u32string utf32data;
+					utf32data.reserve(fc.GetLength() / sizeof(char32_t));
+					while(!fc.EndOfFile())
+					{
+						utf32data.push_back(static_cast<char32_t>(mpt::IO::FileReader::ReadInt32BE(fc)));
+					}
+					filetext = mpt::transcode<mpt::ustring>(utf32data);
+				}
+			}
+			break;
+		case IniEncoding::UTF32LE:
+			{
+				if((textdata.size() % sizeof(char32_t)) != 0)
+				{
+					MPT_LOG_GLOBAL(LogError, "Settings", MPT_UFORMAT_MESSAGE("{}: {}")(filename, U_("UTF32 encoding detected, but file size is not a multiple of 4.")));
+				}
+				MPT_MAYBE_CONSTANT_IF(mpt::endian_is_little())
+				{
+					std::u32string utf32data;
+					std::size_t count = textdata.size() / sizeof(char32_t);
+					utf32data.resize(count);
+					std::memcpy(utf32data.data(), textdata.data(), count * sizeof(char32_t));
+					filetext = mpt::transcode<mpt::ustring>(utf32data);
+				} else
+				{
+					auto fc = mpt::IO::make_FileCursor<mpt::PathString>(textdata);
+					std::u32string utf32data;
+					utf32data.reserve(fc.GetLength() / sizeof(char32_t));
+					while(!fc.EndOfFile())
+					{
+						utf32data.push_back(static_cast<char32_t>(mpt::IO::FileReader::ReadInt32LE(fc)));
+					}
+					filetext = mpt::transcode<mpt::ustring>(utf32data);
+				}
+			}
+			break;
+		case IniEncoding::UTF16BE:
+			{
+				if((textdata.size() % sizeof(char16_t)) != 0)
+				{
+					MPT_LOG_GLOBAL(LogError, "Settings", MPT_UFORMAT_MESSAGE("{}: {}")(filename, U_("UTF16 encoding detected, but file size is not a multiple of 2.")));
+				}
+				MPT_MAYBE_CONSTANT_IF(mpt::endian_is_big())
+				{
+					std::u16string utf16data;
+					std::size_t count = textdata.size() / sizeof(char16_t);
+					utf16data.resize(count);
+					std::memcpy(utf16data.data(), textdata.data(), count * sizeof(char16_t));
+					filetext = mpt::transcode<mpt::ustring>(utf16data);
+				} else
+				{
+					auto fc = mpt::IO::make_FileCursor<mpt::PathString>(textdata);
+					std::u16string utf16data;
+					utf16data.reserve(fc.GetLength() / sizeof(char16_t));
+					while(!fc.EndOfFile())
+					{
+						utf16data.push_back(static_cast<char16_t>(mpt::IO::FileReader::ReadInt16BE(fc)));
+					}
+					filetext = mpt::transcode<mpt::ustring>(utf16data);
+				}
+			}
+			break;
+		case IniEncoding::UTF16LE:
+			{
+				if((textdata.size() % sizeof(char16_t)) != 0)
+				{
+					MPT_LOG_GLOBAL(LogError, "Settings", MPT_UFORMAT_MESSAGE("{}: {}")(filename, U_("UTF16 encoding detected, but file size is not a multiple of 2.")));
+				}
+				MPT_MAYBE_CONSTANT_IF(MPT_OS_WINDOWS)
+				{
+					std::wstring utf16data;
+					std::size_t count = textdata.size() / sizeof(wchar_t);
+					utf16data.resize(count);
+					std::memcpy(utf16data.data(), textdata.data(), count * sizeof(wchar_t));
+					filetext = mpt::transcode<mpt::ustring>(utf16data);
+				} else MPT_MAYBE_CONSTANT_IF(mpt::endian_is_little())
+				{
+					std::u16string utf16data;
+					std::size_t count = textdata.size() / sizeof(char16_t);
+					utf16data.resize(count);
+					std::memcpy(utf16data.data(), textdata.data(), count * sizeof(char16_t));
+					filetext = mpt::transcode<mpt::ustring>(utf16data);
+				} else
+				{
+					auto fc = mpt::IO::make_FileCursor<mpt::PathString>(textdata);
+					std::u16string utf16data;
+					utf16data.reserve(fc.GetLength() / sizeof(char16_t));
+					while(!fc.EndOfFile())
+					{
+						utf16data.push_back(static_cast<char16_t>(mpt::IO::FileReader::ReadInt16LE(fc)));
+					}
+					filetext = mpt::transcode<mpt::ustring>(utf16data);
+				}
+			}
+			break;
+		case IniEncoding::UTF8:
+			filetext = mpt::transcode<mpt::ustring>(mpt::common_encoding::utf8, mpt::buffer_cast<std::string>(textdata));
+			break;
+		case IniEncoding::ANSI:
+			filetext = mpt::transcode<mpt::ustring>(mpt::logical_encoding::locale, mpt::buffer_cast<std::string>(textdata));
+			break;
+	}
+	return filetext;
 }
 
-mpt::PathString WindowsIniFileBase::GetFilename() const
+IniVersion IniFileHelpers::ProbeVersion(const std::vector<mpt::ustring> &lines)
 {
-	return filename;
+	IniVersion result;
+	if(!((lines.size() > 0) && (lines[0] == MPT_ULITERAL("[!Type]"))))
+	{
+		return result;
+	}
+	if(!((lines.size() > 1) && (lines[1] == MPT_ULITERAL("!Format=org.openmpt.fileformat.ini"))))
+	{
+		return result;
+	}
+	// assume our version from now
+	result.major = 4;
+	result.minor = 0;
+	result.patch = 0;
+	if(!(lines.size() > 4))
+	{
+		return result;
+	}
+	std::vector<mpt::ustring> linemajor = mpt::split<mpt::ustring>(lines[2], U_("="));
+	std::vector<mpt::ustring> lineminor = mpt::split<mpt::ustring>(lines[3], U_("="));
+	std::vector<mpt::ustring> linepatch = mpt::split<mpt::ustring>(lines[4], U_("="));
+	if((linemajor.size() != 2) || (linemajor[0] != U_("!VersionMajor")))
+	{
+		return result;
+	}
+	if((lineminor.size() != 2) || (lineminor[0] != U_("!VersionMinor")))
+	{
+		return result;
+	}
+	if((linepatch.size() != 2) || (linepatch[0] != U_("!VersionPatch")))
+	{
+		return result;
+	}
+	result.major = mpt::parse<uint8>(linemajor[1]);
+	result.minor = mpt::parse<uint8>(lineminor[1]);
+	result.patch = mpt::parse<uint8>(linepatch[1]);
+	return result;
 }
 
+std::list<std::pair<SettingPath, SettingValue>> IniFileHelpers::CreateIniHeader(IniVersion version)
+{
+	std::list<std::pair<SettingPath, SettingValue>> result;
+	switch(version.major)
+	{
+		case 1:
+			break;
+		case 2:
+			break;
+		case 3:
+			break;
+		case 4:
+			result.emplace_back(std::make_pair(SettingPath{U_("!Type"), U_("!Format")}, U_("org.openmpt.fileformat.ini")));
+			result.emplace_back(std::make_pair(SettingPath{U_("!Type"), U_("!VersionMajor")}, static_cast<int32>(version.major)));
+			result.emplace_back(std::make_pair(SettingPath{U_("!Type"), U_("!VersionMinor")}, static_cast<int32>(version.minor)));
+			result.emplace_back(std::make_pair(SettingPath{U_("!Type"), U_("!VersionPatch")}, static_cast<int32>(version.patch)));
+			break;
+	}
+	return result;
+}
 
+std::vector<std::byte> IniFileHelpers::EncodeTextWithBOM(IniEncoding encoding_hint, const mpt::ustring &text)
+{
+	std::vector<std::byte> result;
+	if(encoding_hint == IniEncoding::ANSI)
+	{
+		mpt::append(result, mpt::byte_cast<mpt::const_byte_span>(mpt::as_span(mpt::transcode<std::string>(mpt::logical_encoding::locale, text))));
+	} else if((encoding_hint == IniEncoding::UTF32BE) || (encoding_hint == IniEncoding::UTF32LE) || (encoding_hint == IniEncoding::UTF16BE) || (encoding_hint == IniEncoding::UTF16LE))
+	{
+		const std::array<std::byte, 2> utf16le_bom = {mpt::byte_cast<std::byte>(uint8{0xff}), mpt::byte_cast<std::byte>(uint8{0xfe})};
+		mpt::append(result, utf16le_bom);
+		std::wstring wtext = mpt::transcode<std::wstring>(text);
+		mpt::append(result, mpt::as_span(reinterpret_cast<const std::byte*>(wtext.data()), wtext.size() * sizeof(wchar_t)));
+	} else
+	{
+		const std::array<std::byte, 3> utf8_bom = {mpt::byte_cast<std::byte>(uint8{0xef}), mpt::byte_cast<std::byte>(uint8{0xbb}), mpt::byte_cast<std::byte>(uint8{0xbf})};
+		mpt::append(result, utf8_bom);
+		mpt::append(result, mpt::byte_cast<mpt::const_byte_span>(mpt::as_span(mpt::transcode<std::string>(mpt::common_encoding::utf8, text))));
+	}
+	return result;
+}
 
 mpt::winstring IniFileHelpers::GetSection(const SettingPath &path)
 {
@@ -481,6 +748,9 @@
 			if(val[0] == _T('\"') && val[val.length() - 1] == _T('\"'))
 			{
 				val = val.substr(1, val.length() - 2);
+			} else if(val[0] == _T('\'') && val[val.length() - 1] == _T('\''))
+			{
+				val = val.substr(1, val.length() - 2);
 			}
 		}
 		result.insert(std::make_pair(mpt::ToUnicode(key), std::make_optional(mpt::ToUnicode(val))));
@@ -568,4 +838,905 @@
 
 
 
+#if MPT_USTRING_MODE_UTF8
+
+// We cannot escape natively in UTF8 because ^x00 escapes would be different.
+// This has a 1.5x cost in ustring UTF8 mode, but luckily we do not care
+// much at the moment.
+
+static inline bool is_hex(char32_t c)
+{
+	return (U'0' <= c && c <= U'9') || (U'a' <= c && c <= U'f') || (U'A' <= c && c <= U'F');
+}
+
+static mpt::ustring escape(mpt::ustring_view text_)
+{
+	std::u32string text = mpt::transcode<std::u32string>(text_);
+	std::u32string result;
+	const std::size_t len = text.length();
+	result.reserve(len);
+	for(std::size_t pos = 0; pos < len; ++pos)
+	{
+		const char32_t c = text[pos];
+		if((c == U'^') || (c == U';') || (c == U'[') || (c == U']') || (c == U'=') || (c == U'\"') || (c == U'\''))
+		{
+			result.push_back(U'^');
+			result.push_back(c);
+		} else if(mpt::char_value(c) < 32)
+		{
+			result.push_back(U'^');
+			result.push_back(U'x');
+			result.append(mpt::fmtT<std::u32string>::hex0<2>(mpt::char_value(c)));
+		} else if((0x80 <= mpt::char_value(c)) && (mpt::char_value(c) <= 0x9f))
+		{
+			result.push_back(U'^');
+			result.push_back(U'x');
+			result.append(mpt::fmtT<std::u32string>::hex0<2>(mpt::char_value(c)));
+		} else
+		{
+			result.push_back(c);
+		}
+	}
+	return mpt::transcode<mpt::ustring>(result);
+}
+
+static mpt::ustring unescape(mpt::ustring_view text_)
+{
+	std::u32string text = mpt::transcode<std::u32string>(text_);
+	std::u32string result;
+	const std::size_t len = text.length();
+	result.reserve(len);
+	for(std::size_t pos = 0; pos < len; ++pos)
+	{
+		std::size_t rem = len - pos;
+		if(text[pos] == U'^')
+		{
+			if((rem >= 2) && (text[pos+1] == U'^'))
+			{
+				result.push_back(U'^');
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == U';'))
+			{
+				result.push_back(U';');
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == U'['))
+			{
+				result.push_back(U'[');
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == U']'))
+			{
+				result.push_back(U']');
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == U'='))
+			{
+				result.push_back(U'=');
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == U'\"'))
+			{
+				result.push_back(U'\"');
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == U'\''))
+			{
+				result.push_back(U'\'');
+				pos += 1;
+			} else if((rem >= 4) && (text[pos+1] == U'x') && is_hex(text[pos+2]) && is_hex(text[pos+3]))
+			{
+				result.append(std::u32string(1, static_cast<char32_t>(mpt::parse_hex<uint8>(std::u32string{text.substr(pos + 2, 4)}))));
+				pos += 3;
+			} else if((rem >= 6) && (text[pos+1] == U'u') && is_hex(text[pos+2]) && is_hex(text[pos+3]) && is_hex(text[pos+4]) && is_hex(text[pos+5]))
+			{
+				result.append(std::u32string(1, static_cast<char32_t>(mpt::parse_hex<uint16>(std::u32string{text.substr(pos + 2, 4)}))));
+				pos += 5;
+			} else if((rem >= 10) && (text[pos+1] == U'U') && is_hex(text[pos+2]) && is_hex(text[pos+3]) && is_hex(text[pos+4]) && is_hex(text[pos+5]) && is_hex(text[pos+6]) && is_hex(text[pos+7]) && is_hex(text[pos+8]) && is_hex(text[pos+9]))
+			{
+				result.append(std::u32string(1, static_cast<char32_t>(mpt::parse_hex<uint32>(std::u32string{text.substr(pos + 2, 8)}))));
+				pos += 9;
+			} else
+			{
+				result.push_back(text[pos]);
+			}
+		} else
+		{
+			result.push_back(text[pos]);
+		}
+	}
+	return mpt::transcode<mpt::ustring>(result);
+}
+
+#else
+
+static inline bool is_hex(mpt::uchar c)
+{
+	return (MPT_UCHAR('0') <= c && c <= MPT_UCHAR('9')) || (MPT_UCHAR('a') <= c && c <= MPT_UCHAR('f')) || (MPT_UCHAR('A') <= c && c <= MPT_UCHAR('F'));
+}
+
+static mpt::ustring escape(mpt::ustring_view text)
+{
+	mpt::ustring result;
+	const std::size_t len = text.length();
+	result.reserve(len);
+	for(std::size_t pos = 0; pos < len; ++pos)
+	{
+		const mpt::uchar c = text[pos];
+		if((c == MPT_UCHAR('^')) || (c == MPT_UCHAR(';')) || (c == MPT_UCHAR('[')) || (c == MPT_UCHAR(']')) || (c == MPT_UCHAR('=')) || (c == MPT_UCHAR('\"')) || (c == MPT_UCHAR('\'')))
+		{
+			result.push_back(MPT_UCHAR('^'));
+			result.push_back(c);
+		} else if(mpt::char_value(c) < 32)
+		{
+			result.push_back(MPT_UCHAR('^'));
+			result.push_back(MPT_UCHAR('x'));
+			result.append(mpt::ufmt::hex0<2>(mpt::char_value(c)));
+		} else if((0x80 <= mpt::char_value(c)) && (mpt::char_value(c) <= 0x9f))
+		{
+			result.push_back(MPT_UCHAR('^'));
+			result.push_back(MPT_UCHAR('x'));
+			result.append(mpt::ufmt::hex0<2>(mpt::char_value(c)));
+		} else
+		{
+			result.push_back(c);
+		}
+	}
+	return result;
+}
+
+static mpt::ustring unescape(mpt::ustring_view text)
+{
+	mpt::ustring result;
+	const std::size_t len = text.length();
+	result.reserve(len);
+	for(std::size_t pos = 0; pos < len; ++pos)
+	{
+		std::size_t rem = len - pos;
+		if(text[pos] == MPT_UCHAR('^'))
+		{
+			if((rem >= 2) && (text[pos+1] == MPT_UCHAR('^')))
+			{
+				result.push_back(MPT_UCHAR('^'));
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == MPT_UCHAR(';')))
+			{
+				result.push_back(MPT_UCHAR(';'));
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('[')))
+			{
+				result.push_back(MPT_UCHAR('['));
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == MPT_UCHAR(']')))
+			{
+				result.push_back(MPT_UCHAR(']'));
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('=')))
+			{
+				result.push_back(MPT_UCHAR('='));
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('\"')))
+			{
+				result.push_back(MPT_UCHAR('\"'));
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('\'')))
+			{
+				result.push_back(MPT_UCHAR('\''));
+				pos += 1;
+			} else if((rem >= 4) && (text[pos+1] == MPT_UCHAR('x')) && is_hex(text[pos+2]) && is_hex(text[pos+3]))
+			{
+				result.push_back(static_cast<mpt::uchar>(mpt::parse_hex<uint8>(mpt::ustring{text.substr(pos + 2, 2)})));
+				pos += 3;
+			} else if((rem >= 6) && (text[pos+1] == MPT_UCHAR('u')) && is_hex(text[pos+2]) && is_hex(text[pos+3]) && is_hex(text[pos+4]) && is_hex(text[pos+5]))
+			{
+				result.append(mpt::transcode<mpt::ustring>(std::u16string(1, static_cast<char16_t>(mpt::parse_hex<uint16>(mpt::ustring{text.substr(pos + 2, 4)})))));
+				pos += 5;
+			} else if((rem >= 10) && (text[pos+1] == MPT_UCHAR('U')) && is_hex(text[pos+2]) && is_hex(text[pos+3]) && is_hex(text[pos+4]) && is_hex(text[pos+5]) && is_hex(text[pos+6]) && is_hex(text[pos+7]) && is_hex(text[pos+8]) && is_hex(text[pos+9]))
+			{
+				result.append(mpt::transcode<mpt::ustring>(std::u32string(1, static_cast<char32_t>(mpt::parse_hex<uint32>(mpt::ustring{text.substr(pos + 2, 8)})))));
+				pos += 9;
+			} else
+			{
+				result.push_back(text[pos]);
+			}
+		} else
+		{
+			result.push_back(text[pos]);
+		}
+	}
+	return result;
+}
+
+#endif
+
+static inline std::size_t find_unescaped_first(mpt::ustring_view text, mpt::uchar c)
+{
+	const std::size_t len = text.length();
+	for(std::size_t pos = 0; pos < len; ++pos)
+	{
+		if(text[pos] == c)
+		{
+			return pos;
+		}
+		std::size_t rem = len - pos;
+		if(text[pos] == MPT_UCHAR('^'))
+		{
+			if((rem >= 2) && (text[pos+1] == MPT_UCHAR('^')))
+			{
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == MPT_UCHAR(';')))
+			{
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('[')))
+			{
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == MPT_UCHAR(']')))
+			{
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('=')))
+			{
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('\"')))
+			{
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('\'')))
+			{
+				pos += 1;
+			} else if((rem >= 4) && (text[pos+1] == MPT_UCHAR('x')) && is_hex(text[pos+2]) && is_hex(text[pos+3]))
+			{
+				pos += 3;
+			} else if((rem >= 6) && (text[pos+1] == MPT_UCHAR('u')) && is_hex(text[pos+2]) && is_hex(text[pos+3]) && is_hex(text[pos+4]) && is_hex(text[pos+5]))
+			{
+				pos += 5;
+			} else if((rem >= 10) && (text[pos+1] == MPT_UCHAR('U')) && is_hex(text[pos+2]) && is_hex(text[pos+3]) && is_hex(text[pos+4]) && is_hex(text[pos+5]) && is_hex(text[pos+6]) && is_hex(text[pos+7]) && is_hex(text[pos+8]) && is_hex(text[pos+9]))
+			{
+				pos += 9;
+			}
+		}
+	}
+	return mpt::ustring_view::npos;
+}
+
+static inline std::size_t find_unescaped_last(mpt::ustring_view text, mpt::uchar c)
+{
+	std::size_t result = mpt::ustring_view::npos;
+	const std::size_t len = text.length();
+	for(std::size_t pos = 0; pos < len; ++pos)
+	{
+		if(text[pos] == c)
+		{
+			result = pos;
+		}
+		std::size_t rem = len - pos;
+		if(text[pos] == MPT_UCHAR('^'))
+		{
+			if((rem >= 2) && (text[pos+1] == MPT_UCHAR('^')))
+			{
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == MPT_UCHAR(';')))
+			{
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('[')))
+			{
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == MPT_UCHAR(']')))
+			{
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('=')))
+			{
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('\"')))
+			{
+				pos += 1;
+			} else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('\'')))
+			{
+				pos += 1;
+			} else if((rem >= 4) && (text[pos+1] == MPT_UCHAR('x')) && is_hex(text[pos+2]) && is_hex(text[pos+3]))
+			{
+				pos += 3;
+			} else if((rem >= 6) && (text[pos+1] == MPT_UCHAR('u')) && is_hex(text[pos+2]) && is_hex(text[pos+3]) && is_hex(text[pos+4]) && is_hex(text[pos+5]))
+			{
+				pos += 5;
+			} else if((rem >= 10) && (text[pos+1] == MPT_UCHAR('U')) && is_hex(text[pos+2]) && is_hex(text[pos+3]) && is_hex(text[pos+4]) && is_hex(text[pos+5]) && is_hex(text[pos+6]) && is_hex(text[pos+7]) && is_hex(text[pos+8]) && is_hex(text[pos+9]))
+			{
+				pos += 9;
+			}
+		}
+	}
+	return result;
+}
+
+static std::vector<mpt::ustring> split_any_line_ending(mpt::ustring_view text)
+{
+	std::vector<mpt::ustring> result;
+	const std::size_t len = text.length();
+	std::size_t beg = 0;
+	for(std::size_t pos = 0; pos < len; ++pos)
+	{
+		std::size_t rem = len - pos;
+		if((rem >= 2) && (text[pos+0] == MPT_UCHAR('\r')) && (text[pos+1] == MPT_UCHAR('\n')))
+		{
+			result.emplace_back(text.substr(beg, pos - beg));
+			pos += 2;
+			beg = pos;
+		} else if((rem >=1) && text[pos+0] == MPT_UCHAR('\n'))
+		{
+			result.emplace_back(text.substr(beg, pos - beg));
+			pos += 1;
+			beg = pos;
+		} else if((rem >=1) && text[pos+0] == MPT_UCHAR('\r'))
+		{
+			result.emplace_back(text.substr(beg, pos - beg));
+			pos += 1;
+			beg = pos;
+		}
+	}
+	if(!result.empty() || ((len - beg) > 0))
+	{
+		result.emplace_back(text.substr(beg, len - beg));
+	}
+	return result;
+}
+
+static inline mpt::ustring_view trim_whitespace(mpt::ustring_view text)
+{
+	mpt::ustring_view::size_type beg_pos = text.find_first_not_of(MPT_ULITERAL(" \t\r\n"));
+	if(beg_pos != mpt::ustring_view::npos)
+	{
+		text = text.substr(beg_pos);
+	} else
+	{
+		text = text.substr(text.length(), 0);
+	}
+	mpt::ustring_view::size_type end_pos = text.find_last_not_of(MPT_ULITERAL(" \t\r\n"));
+	if(end_pos != mpt::ustring_view::npos)
+	{
+		text = text.substr(0, end_pos + 1);
+	} else
+	{
+		text = text.substr(0, 0);
+	}
+	return text;
+}
+
+static inline mpt::ustring_view remove_quotes(mpt::ustring_view text)
+{
+	if(text.length() <= 1)
+	{
+		return text;
+	}
+	{
+		mpt::ustring_view::size_type beg_quote = find_unescaped_first(text, MPT_UCHAR('\"'));
+		mpt::ustring_view::size_type end_quote = find_unescaped_last(text, MPT_UCHAR('\"'));
+		if((beg_quote == 0) && (end_quote == (text.length() - 1)))
+		{
+			return text.substr(1, text.length() - 2);
+		}
+	}
+	{
+		mpt::ustring_view::size_type beg_quote = find_unescaped_first(text, MPT_UCHAR('\''));
+		mpt::ustring_view::size_type end_quote = find_unescaped_last(text, MPT_UCHAR('\''));
+		if((beg_quote == 0) && (end_quote == (text.length() - 1)))
+		{
+			return text.substr(1, text.length() - 2);
+		}
+	}
+	return text;
+}
+
+#if MPT_SETTINGS_INI_CASE_INSENSITIVE
+static mpt::ustring lowercase(mpt::ustring_view text)
+{
+	return mpt::ToLowerCase(mpt::ustring{text});
+}
+#endif
+
+void CachedIniFileSettingsBackend::ReadFileIntoCache()
+{
+	cache.clear();
+#if MPT_SETTINGS_INI_CASE_INSENSITIVE
+	if(case_insensitive)
+	{
+		casemap.clear();
+	}
+#endif
+	const std::vector<mpt::ustring> lines = split_any_line_ending(DecodeTextWithBOM(file.read(), filename));
+	const IniVersion version = ProbeVersion(lines);
+	mpt::ustring current_section;
+	mpt::ustring last_key;
+	bool empty_line_after_last_key = false;
+	std::vector<mpt::ustring> running_comments;
+	std::size_t line_number = 0;
+	bool last_line_empty = false;
+	const bool quirk_no_unescape = (version.major < 4);
+	auto store_previous_comments = [&]()
+		{
+			if(!empty_line_after_last_key && (running_comments.size() > 0))
+			{
+				comments[std::make_pair(current_section, last_key)].after = std::move(running_comments);
+				running_comments = {};
+			}
+		};
+	for(const auto &raw_line : lines)
+	{
+		line_number += 1;
+		last_line_empty = (raw_line.length() == 0);
+		mpt::ustring_view line = trim_whitespace(raw_line);
+		std::size_t line_len = line.length();
+		if(line_len == 0)
+		{ // empty line
+			store_previous_comments();
+			empty_line_after_last_key = true;
+			continue;
+		}
+		if(find_unescaped_first(line, MPT_UCHAR(';')) == 0)
+		{ // comment
+			running_comments.push_back(mpt::ustring{trim_whitespace(line.substr(1))});
+			continue;
+		}
+		if(find_unescaped_first(line, MPT_UCHAR('[')) == 0)
+		{ // section start
+			mpt::ustring_view::size_type opening_pos = 0;
+			mpt::ustring_view::size_type closing_pos = find_unescaped_first(line, MPT_UCHAR(']'));
+			if(closing_pos == mpt::ustring_view::npos)
+			{
+				store_previous_comments();
+				empty_line_after_last_key = true;
+				MPT_LOG_GLOBAL(LogError, "Settings", MPT_UFORMAT_MESSAGE("{}({}): No closing bracket in section header. ('{}').")(filename, line_number, raw_line));
+				continue;
+			}
+			mpt::ustring_view remainder = line.substr(closing_pos + 1);
+			if(trim_whitespace(remainder).length() > 0)
+			{
+				MPT_LOG_GLOBAL(LogWarning, "Settings", MPT_UFORMAT_MESSAGE("{}({}): Junk after section header. ('{}').")(filename, line_number, raw_line));
+			}
+			mpt::ustring_view escaped_section = trim_whitespace(line.substr(opening_pos + 1, closing_pos - opening_pos - 1));
+			mpt::ustring section = (quirk_no_unescape ? mpt::ustring{escaped_section} : unescape(escaped_section));
+#if MPT_SETTINGS_INI_CASE_INSENSITIVE
+			if(case_insensitive)
+			{
+				if(casemap.find(std::make_pair(lowercase(section), mpt::ustring{})) != casemap.end())
+				{
+					store_previous_comments();
+					empty_line_after_last_key = true;
+					current_section.clear();
+					MPT_LOG_GLOBAL(LogWarning, "Settings", MPT_UFORMAT_MESSAGE("{}({}): Ignoring duplicate casefolded section '{}'. ('{}').")(filename, line_number, escape(section), raw_line));
+					continue;
+				}
+				casemap.insert(std::make_pair(std::make_pair(lowercase(section), mpt::ustring{}), std::make_pair(section, mpt::ustring{})));
+			}
+#endif
+			if(cache.find(section) != cache.end())
+			{
+				store_previous_comments();
+				empty_line_after_last_key = true;
+				current_section.clear();
+				MPT_LOG_GLOBAL(LogWarning, "Settings", MPT_UFORMAT_MESSAGE("{}({}): Ignoring duplicate section '{}'. ('{}').")(filename, line_number, escape(section), raw_line));
+				continue;
+			}
+			MPT_LOG_GLOBAL(LogInformation, "Settings", MPT_UFORMAT_MESSAGE("{}({}): [{}]")(filename, line_number, escape(section)));
+			comments[std::make_pair(section, mpt::ustring{})].before = std::move(running_comments);
+			running_comments = {};
+			cache[section].emplace();
+			current_section = std::move(section);
+			last_key.clear();
+			empty_line_after_last_key = false;
+			continue;
+		}
+		mpt::ustring_view::size_type equal_pos = find_unescaped_first(line, MPT_UCHAR('='));
+		if(equal_pos == mpt::ustring_view::npos)
+		{ // syntax error
+			store_previous_comments();
+			empty_line_after_last_key = true;
+			MPT_LOG_GLOBAL(LogError, "Settings", MPT_UFORMAT_MESSAGE("{}({}): Syntax error: Invalid token. ('{}').")(filename, line_number, raw_line));
+			continue;
+		}
+		// key value
+		if(current_section.empty())
+		{
+			store_previous_comments();
+			empty_line_after_last_key = true;
+			MPT_LOG_GLOBAL(LogWarning, "Settings", MPT_UFORMAT_MESSAGE("{}({}): Ignoring key-value without section. ('{}').")(filename, line_number, raw_line));
+			continue;
+		}
+		mpt::ustring_view escaped_key = trim_whitespace(line.substr(0, equal_pos));
+		mpt::ustring_view escaped_val = remove_quotes(trim_whitespace(line.substr(equal_pos + 1)));
+		if(escaped_key.empty())
+		{
+			store_previous_comments();
+			empty_line_after_last_key = true;
+			MPT_LOG_GLOBAL(LogError, "Settings", MPT_UFORMAT_MESSAGE("{}({}): Syntax error: Empty key. ('{}').")(filename, line_number, raw_line));
+			continue;
+		}
+		mpt::ustring key = (quirk_no_unescape ? mpt::ustring{escaped_key} : unescape(escaped_key));
+		mpt::ustring val = (quirk_no_unescape ? mpt::ustring{escaped_val} : unescape(escaped_val));
+#if MPT_SETTINGS_INI_CASE_INSENSITIVE
+		if(case_insensitive)
+		{
+			if(casemap.find(std::make_pair(lowercase(current_section), lowercase(key))) != casemap.end())
+			{
+				store_previous_comments();
+				empty_line_after_last_key = true;
+				MPT_LOG_GLOBAL(LogWarning, "Settings", MPT_UFORMAT_MESSAGE("{}({}): Ignoring duplicate casefolded key '{}'. ('{}').")(filename, line_number, escape(key), raw_line));
+				continue;
+			}
+		}
+#endif
+		if(cache[current_section].value().find(key) != cache[current_section].value().end())
+		{
+			store_previous_comments();
+			empty_line_after_last_key = true;
+			MPT_LOG_GLOBAL(LogWarning, "Settings", MPT_UFORMAT_MESSAGE("{}({}): Ignoring duplicate key '{}'. ('{}').")(filename, line_number, escape(key), raw_line));
+			continue;
+		}
+		MPT_LOG_GLOBAL(LogInformation, "Settings", MPT_UFORMAT_MESSAGE("{}({}): {}={}")(filename, line_number, escape(key), escape(val)));
+		comments[std::make_pair(current_section, key)].before = std::move(running_comments);
+		running_comments = {};
+#if MPT_SETTINGS_INI_CASE_INSENSITIVE
+		if(case_insensitive)
+		{
+			casemap.insert(std::make_pair(std::make_pair(lowercase(current_section), lowercase(key)), std::make_pair(current_section, key)));
+		}
+#endif
+		cache[current_section].value()[key] = std::make_optional(std::move(val));
+		last_key = std::move(key);
+		empty_line_after_last_key = false;
+	}
+	store_previous_comments();
+	if(!last_line_empty)
+	{
+		MPT_LOG_GLOBAL(LogNotification, "Settings", MPT_UFORMAT_MESSAGE("{}({}): {}")(filename, line_number, U_("No newline after last line.")));
+	}
+#if MPT_SETTINGS_INI_CASE_INSENSITIVE
+	if(case_insensitive)
+	{
+		VerifyCasemap();
+	}
+#endif
+}
+
+void CachedIniFileSettingsBackend::MergeSettingsIntoCache(const std::set<mpt::ustring> &removeSections, const std::map<SettingPath, std::optional<SettingValue>> &settings)
+{
+#if MPT_SETTINGS_INI_CASE_INSENSITIVE
+	if(case_insensitive)
+	{
+		for(const auto &section : removeSections)
+		{
+			const auto mapping = casemap.find(std::make_pair(lowercase(section), mpt::ustring{}));
+			if(mapping != casemap.end())
+			{
+				if(mapping->second.first != section)
+				{
+					const auto entry = cache.find(mapping->second.first);
+					if(entry != cache.end())
+					{
+						auto node = cache.extract(entry);
+						node.key() = section;
+						cache.insert(std::move(node));
+						auto casenode = casemap.extract(mapping);
+						mapping->second.first = section;
+					}
+				}
+			}
+		}
+	}
+#endif
+	for(const auto &section : removeSections)
+	{
+		cache.erase(section);
+#if MPT_SETTINGS_INI_CASE_INSENSITIVE
+		if(case_insensitive)
+		{
+			casemap.erase(std::make_pair(lowercase(section), mpt::ustring{}));
+		}
+#endif
+	}
+	std::map<mpt::ustring, std::map<SettingPath, std::optional<SettingValue>>> sectionssettings;
+	for(const auto &[path, value] : settings)
+	{
+		sectionssettings[path.GetRefSection()][path] = value;
+	}
+	for(const auto &[section, sectionsettings] : sectionssettings)
+	{
+#if MPT_SETTINGS_INI_CASE_INSENSITIVE
+		if(case_insensitive)
+		{
+			const auto mapping = casemap.find(std::make_pair(lowercase(section), mpt::ustring{}));
+			if(mapping == casemap.end())
+			{
+				casemap.insert(std::make_pair(std::make_pair(lowercase(section), mpt::ustring{}), std::make_pair(section, mpt::ustring{})));
+			} else
+			{
+				if(mapping->second.first != section)
+				{
+					const auto entry = cache.find(mapping->second.first);
+					if(entry != cache.end())
+					{
+						auto node = cache.extract(entry);
+						node.key() = section;
+						cache.insert(std::move(node));
+						auto casenode = casemap.extract(mapping);
+						mapping->second.first = section;
+					}
+				}
+			}
+		}
+#endif
+		if(!cache[section].has_value())
+		{
+			cache[section].emplace();
+		}
+		std::map<mpt::ustring, std::optional<mpt::ustring>> &sectioncache = cache[section].value();
+		for(const auto &[path, value] : sectionsettings)
+		{
+#if MPT_SETTINGS_INI_CASE_INSENSITIVE
+			if(case_insensitive)
+			{
+				const auto mapping = casemap.find(std::make_pair(lowercase(section), lowercase(path.GetRefKey())));
+				if(mapping == casemap.end())
+				{
+					casemap.insert(std::make_pair(std::make_pair(lowercase(section), lowercase(path.GetRefKey())), std::make_pair(section, path.GetRefKey())));
+				} else
+				{
+					if(mapping->second.second != path.GetRefKey())
+					{
+						const auto entry = sectioncache.find(mapping->second.second);
+						if(entry != sectioncache.end())
+						{
+							auto node = sectioncache.extract(entry);
+							node.key() = path.GetRefKey();
+							sectioncache.insert(std::move(node));
+							auto casenode = casemap.extract(mapping);
+							mapping->second.second = path.GetRefKey();
+						}
+					}
+				}
+			}
+#endif
+			if(value.has_value())
+			{
+				sectioncache[path.GetRefKey()] = FormatValueAsIni(value.value());
+			} else
+			{
+				sectioncache.erase(path.GetRefKey());
+#if MPT_SETTINGS_INI_CASE_INSENSITIVE
+				if(case_insensitive)
+				{
+					casemap.erase(std::make_pair(lowercase(section), lowercase(path.GetRefKey())));
+				}
+#endif
+			}
+		}
+	}
+#if MPT_SETTINGS_INI_CASE_INSENSITIVE
+	if(case_insensitive)
+	{
+		VerifyCasemap();
+	}
+#endif
+}
+
+void CachedIniFileSettingsBackend::WriteCacheIntoFile()
+{
+	mpt::ustring filetext;
+	const std::list<std::pair<SettingPath, SettingValue>> header = CreateIniHeader(IniVersion{4, 0, 0});
+	for(const auto &path : header)
+	{
+		cache.erase(path.first.GetRefSection());
+	}
+	const mpt::uchar *newline = MPT_ULITERAL("\r\n");
+	mpt::ustring last_section;
+	for(const auto &[path, value] : header)
+	{
+		if(path.GetRefSection() != last_section)
+		{
+			last_section = path.GetRefSection();
+			filetext += MPT_UFORMAT_MESSAGE("[{}]{}")(escape(path.GetRefSection()), newline);
+		}
+		filetext += MPT_UFORMAT_MESSAGE("{}={}{}")(escape(path.GetRefKey()), escape(FormatValueAsIni(value)), newline);
+	}
+	for(const auto &[section, sectionsettings] : cache)
+	{
+		filetext += newline;
+		auto sectioncomments = comments.find(std::make_pair(section, mpt::ustring{}));
+		if(sectioncomments != comments.end())
+		{
+			for(const auto &comment : sectioncomments->second.before)
+			{
+				filetext += MPT_UFORMAT_MESSAGE(";{}{}")(comment, newline);
+			}
+		}
+		filetext += MPT_UFORMAT_MESSAGE("[{}]{}")(escape(section), newline);
+		if(sectioncomments != comments.end())
+		{
+			for(const auto &comment : sectioncomments->second.after)
+			{
+				filetext += MPT_UFORMAT_MESSAGE(";{}{}")(comment, newline);
+			}
+			if(sectioncomments->second.after.size() > 0)
+			{
+				filetext += newline;
+			}
+		}
+		if(sectionsettings.has_value())
+		{
+			for(const auto &[key, value] : sectionsettings.value())
+			{
+				if(value.has_value())
+				{
+					auto keycomments = comments.find(std::make_pair(section, key));
+					if(keycomments != comments.end())
+					{
+						for(const auto &comment : keycomments->second.before)
+						{
+							filetext += MPT_UFORMAT_MESSAGE(";{}{}")(comment, newline);
+						}
+					}
+					const mpt::ustring escaped_value = escape(value.value());
+					if(escaped_value != trim_whitespace(escaped_value))
+					{
+						filetext += MPT_UFORMAT_MESSAGE("{}=\"{}\"{}")(escape(key), escaped_value, newline);
+					} else
+					{
+						filetext += MPT_UFORMAT_MESSAGE("{}={}{}")(escape(key), escaped_value, newline);
+					}
+					if(keycomments != comments.end())
+					{
+						for(const auto &comment : keycomments->second.after)
+						{
+							filetext += MPT_UFORMAT_MESSAGE(";{}{}")(comment, newline);
+						}
+						if(keycomments->second.after.size() > 0)
+						{
+							filetext += newline;
+						}
+					}
+				}
+			}
+		}
+	}
+	IniEncoding encoding = IniEncoding::UTF8;
+#if MPT_OS_WINDOWS && defined(UNICODE)
+	if(mpt::OS::Windows::IsWine())
+	{
+		encoding = IniEncoding::UTF8;
+	} else
+	{
+		encoding = IniEncoding::UTF16LE;
+	}
+#else
+	encoding = IniEncoding::UTF8;
+#endif
+	file.write(EncodeTextWithBOM(encoding, filetext), false);
+}
+
+#if MPT_SETTINGS_INI_CASE_INSENSITIVE
+void CachedIniFileSettingsBackend::VerifyCasemap() const
+{
+#ifndef NDEBUG
+	if(case_insensitive)
+	{
+		for(const auto &[section, optkeyvalues] : cache)
+		{
+			MPT_ASSERT(casemap.find(std::make_pair(lowercase(section), mpt::ustring{})) != casemap.end());
+			MPT_ASSERT(casemap.find(std::make_pair(lowercase(section), mpt::ustring{}))->second == std::make_pair(section, mpt::ustring{}));
+			if(optkeyvalues.has_value())
+			{
+				const auto &sectioncache = optkeyvalues.value();
+				for(const auto &[key, value] : sectioncache)
+				{
+					MPT_ASSERT(casemap.find(std::make_pair(lowercase(section), lowercase(key))) != casemap.end());
+					MPT_ASSERT(casemap.find(std::make_pair(lowercase(section), lowercase(key)))->second == std::make_pair(section, key));
+				}
+			}
+		}
+		for(const auto &[folded, unfolded] : casemap)
+		{
+			MPT_ASSERT(folded.first == lowercase(unfolded.first));
+			MPT_ASSERT(folded.second == lowercase(unfolded.second));
+			MPT_ASSERT(cache.find(unfolded.first) != cache.end());
+			if(folded.second != mpt::ustring{})
+			{
+				MPT_ASSERT(cache.find(unfolded.first)->second.has_value());
+				MPT_ASSERT(cache.find(unfolded.first)->second.value().find(unfolded.second) != cache.find(unfolded.first)->second.value().end());
+			}
+		}
+	}
+#endif
+}
+
+#endif
+
+void CachedIniFileSettingsBackend::InvalidateCache()
+{
+	OPENMPT_PROFILE_FUNCTION(Profiler::Settings);
+	std::lock_guard l(file);
+	ReadFileIntoCache();
+}
+
+SettingValue CachedIniFileSettingsBackend::ReadSetting(const SettingPath &path, const SettingValue &def) const
+{
+	OPENMPT_PROFILE_FUNCTION(Profiler::Settings);
+#if MPT_SETTINGS_INI_CASE_INSENSITIVE
+	if(case_insensitive)
+	{
+		const auto mapping = casemap.find(std::make_pair(lowercase(path.GetRefSection()), lowercase(path.GetRefKey())));
+		if(mapping == casemap.end())
+		{
+			return def;
+		}
+		const SettingPath path2 = SettingPath{mapping->second.first, mapping->second.second};
+		const auto sectionit = cache.find(path2.GetRefSection());
+		if(sectionit == cache.end())
+		{
+			return def;
+		}
+		if(!sectionit->second.has_value())
+		{
+			return def;
+		}
+		const std::map<mpt::ustring, std::optional<mpt::ustring>> &section = sectionit->second.value();
+		const auto it = section.find(path2.GetRefKey());
+		if(it == section.end())
+		{
+			return def;
+		}
+		if(!it->second.has_value())
+		{
+			return def;
+		}
+		return ParseValueFromIni(it->second.value(), def);
+	}
+#endif
+	const auto sectionit = cache.find(path.GetRefSection());
+	if(sectionit == cache.end())
+	{
+		return def;
+	}
+	if(!sectionit->second.has_value())
+	{
+		return def;
+	}
+	const std::map<mpt::ustring, std::optional<mpt::ustring>> &section = sectionit->second.value();
+	const auto it = section.find(path.GetRefKey());
+	if(it == section.end())
+	{
+		return def;
+	}
+	if(!it->second.has_value())
+	{
+		return def;
+	}
+	return ParseValueFromIni(it->second.value(), def);
+}
+
+void CachedIniFileSettingsBackend::WriteAllSettings(const std::set<mpt::ustring> &removeSections, const std::map<SettingPath, std::optional<SettingValue>> &settings)
+{
+	OPENMPT_PROFILE_FUNCTION(Profiler::Settings);
+	std::lock_guard l(file);
+	ReadFileIntoCache();
+	MergeSettingsIntoCache(removeSections, settings);
+	WriteCacheIntoFile();
+}
+
+#if MPT_SETTINGS_INI_CASE_INSENSITIVE
+CachedIniFileSettingsBackend::CachedIniFileSettingsBackend(mpt::PathString filename_, bool case_insensitive_)
+	: IniFileBase(std::move(filename_))
+	, case_insensitive(case_insensitive_)
+{
+	OPENMPT_PROFILE_FUNCTION(Profiler::Settings);
+	std::lock_guard l(file);
+	ReadFileIntoCache();
+}
+#else
+CachedIniFileSettingsBackend::CachedIniFileSettingsBackend(mpt::PathString filename_)
+	: IniFileBase(std::move(filename_))
+{
+	OPENMPT_PROFILE_FUNCTION(Profiler::Settings);
+	std::lock_guard l(file);
+	ReadFileIntoCache();
+}
+#endif
+
+CachedIniFileSettingsBackend::~CachedIniFileSettingsBackend()
+{
+	return;
+}
+
+
 OPENMPT_NAMESPACE_END
Index: mptrack/SettingsIni.h
===================================================================
--- mptrack/SettingsIni.h	(revision 24613)
+++ mptrack/SettingsIni.h	(working copy)
@@ -15,11 +15,15 @@
 
 #include "Settings.h"
 
+#include "mpt/base/integer.hpp"
+#include "mpt/string/types.hpp"
 #include "mpt/io_file_atomic/atomic_file.hpp"
 
+#include <list>
 #include <map>
 #include <optional>
 #include <set>
+#include <utility>
 #include <vector>
 
 
@@ -26,9 +30,41 @@
 OPENMPT_NAMESPACE_BEGIN
 
 
+#define MPT_SETTINGS_INI_CASE_INSENSITIVE 1
+//#define MPT_SETTINGS_INI_CASE_INSENSITIVE 0
+
+
+// Version 1: ANSI
+// Version 2: UTF-16LE with BOM
+// Version 3: UTF-8 with BOM (Wine-only, not supported by Windows)
+// Version 4: any encoding with BOM and explicit version
+
+struct IniVersion
+{
+	uint8 major = 0;
+	uint8 minor = 0;
+	uint8 patch = 0;
+};
+
+
+enum class IniEncoding
+{
+	UTF32BE,
+	UTF32LE,
+	UTF16BE,
+	UTF16LE,
+	UTF8,
+	ANSI,
+};
+
+
 class IniFileHelpers
 {
 protected:
+	static mpt::ustring DecodeTextWithBOM(mpt::const_byte_span filedata, const mpt::PathString &filename);
+	static IniVersion ProbeVersion(const std::vector<mpt::ustring> &lines);
+	static std::list<std::pair<SettingPath, SettingValue>> CreateIniHeader(IniVersion version);
+	static std::vector<std::byte> EncodeTextWithBOM(IniEncoding encoding_hint, const mpt::ustring &text);
 	static mpt::winstring GetSection(const SettingPath &path);
 	static mpt::winstring GetKey(const SettingPath &path);
 	static mpt::ustring FormatValueAsIni(const SettingValue &value);
@@ -36,19 +72,28 @@
 };
 
 
-class WindowsIniFileBase
+class IniFileBase
 {
 protected:
 	const mpt::PathString filename;
 	mpt::IO::atomic_shared_file_ref file;
 protected:
+	IniFileBase(mpt::PathString filename_);
+	~IniFileBase() = default;
+public:
+	const mpt::PathString &Filename() const;
+	mpt::PathString GetFilename() const;
+};
+
+
+class WindowsIniFileBase
+	: public IniFileBase
+{
+protected:
 	WindowsIniFileBase(mpt::PathString filename_);
 	~WindowsIniFileBase() = default;
 public:
 	void ConvertToUnicode(const mpt::ustring &backupTag = mpt::ustring());
-public:
-	const mpt::PathString &Filename() const;
-	mpt::PathString GetFilename() const;
 };
 
 
@@ -105,13 +150,52 @@
 };
 
 
+class CachedIniFileSettingsBackend
+	: public ISettingsBackend<SettingsBatching::All>
+	, public IniFileBase
+	, protected IniFileHelpers
+{
+private:
+	struct comments
+	{
+		std::vector<mpt::ustring> before;
+		std::vector<mpt::ustring> after;
+	};
+private:
+	std::map<mpt::ustring, std::optional<std::map<mpt::ustring, std::optional<mpt::ustring>>>> cache;
+	std::map<std::pair<mpt::ustring, mpt::ustring>, comments> comments;
+#if MPT_SETTINGS_INI_CASE_INSENSITIVE
+	const bool case_insensitive;
+	std::map<std::pair<mpt::ustring, mpt::ustring>, std::pair<mpt::ustring, mpt::ustring>> casemap;
+#endif
+private:
+	void ReadFileIntoCache();
+	void MergeSettingsIntoCache(const std::set<mpt::ustring> &removeSections, const std::map<SettingPath, std::optional<SettingValue>> &settings);
+	void WriteCacheIntoFile();
+#if MPT_SETTINGS_INI_CASE_INSENSITIVE
+	void VerifyCasemap() const;
+#endif
+public:
+#if MPT_SETTINGS_INI_CASE_INSENSITIVE
+	CachedIniFileSettingsBackend(mpt::PathString filename_, bool case_insensitive = false);
+#else
+	CachedIniFileSettingsBackend(mpt::PathString filename_);
+#endif
+	~CachedIniFileSettingsBackend() override;
+public:
+	virtual void InvalidateCache() override;
+	virtual SettingValue ReadSetting(const SettingPath &path, const SettingValue &def) const override;
+	virtual void WriteAllSettings(const std::set<mpt::ustring> &removeSections, const std::map<SettingPath, std::optional<SettingValue>> &settings) override;
+};
+
+
 #ifndef IniFileSettingsBackend
-#define IniFileSettingsBackend ImmediateWindowsIniFileSettingsBackend
+#define IniFileSettingsBackend CachedIniFileSettingsBackend
 #endif
 
 
 #ifndef IniFileSettingsContainer
-#define IniFileSettingsContainer FileSettingsContainer<ImmediateWindowsIniFileSettingsBackend>
+#define IniFileSettingsContainer FileSettingsContainer<CachedIniFileSettingsBackend>
 #endif
 
 
Index: test/test.cpp
===================================================================
--- test/test.cpp	(revision 24613)
+++ test/test.cpp	(working copy)
@@ -3021,6 +3021,29 @@
 		DeleteFile(mpt::support_long_path(filename.AsNative()).c_str());
 	}
 
+	// case
+	#if 0
+	{
+		DeleteFile(mpt::support_long_path(filename.AsNative()).c_str());
+		{
+			mpt::IO::SafeOutputFile outputfile{filename, std::ios::binary};
+			mpt::IO::ofstream & outputstream = outputfile.stream();
+			mpt::IO::WriteTextCRLF(outputstream, " [ \"Test\" ] ");
+			mpt::IO::WriteTextCRLF(outputstream, mpt::ToCharset(mpt::Charset::UTF8, U_("Foo=a")));
+			mpt::IO::WriteTextCRLF(outputstream, mpt::ToCharset(mpt::Charset::UTF8, U_("foo=b")));
+		}
+		{
+			Backend inifile{filename};
+			VERIFY_EQUAL(inifile.ReadSetting(SettingPath{U_("Test"), U_("Foo")}, U_("")).as<mpt::ustring>(), U_("a"));
+		}
+		{
+			Backend inifile{filename};
+			VERIFY_EQUAL(inifile.ReadSetting(SettingPath{U_("Test"), U_("foo")}, U_("")).as<mpt::ustring>(), U_("a"));
+		}
+		DeleteFile(mpt::support_long_path(filename.AsNative()).c_str());
+	}
+	#endif
+
 }
 
 #endif // MODPLUG_TRACKER
@@ -3042,6 +3065,8 @@
 
 	TestIniSettingsBackendRead<BatchedWindowsIniFileSettingsBackend>(filename);
 
+	TestIniSettingsBackendRead<CachedIniFileSettingsBackend>(filename);
+
 	{
 		IniFileSettingsContainer conf{filename};
 		int32 foobar = conf.Read(U_("Test"), U_("bar"), 23);
@@ -3134,7 +3159,7 @@
 
 	{
 		{
-			IniFileSettingsContainer conf{filename};
+			FileSettingsContainer<ImmediateWindowsIniFileSettingsBackend> conf{filename};
 			conf.Write<mpt::ustring>(SettingPath(MPT_USTRING("Test"), MPT_USTRING("spacespacecharspacespace")), spacespacecharspacespace);
 			conf.Write<mpt::ustring>(SettingPath(MPT_USTRING("Test"), MPT_USTRING("threespaces")), threespaces);
 			conf.Write<mpt::ustring>(SettingPath(MPT_USTRING("Test"), MPT_USTRING("tab")), tab);
@@ -3143,7 +3168,7 @@
 			conf.Write<mpt::ustring>(SettingPath(MPT_USTRING("Test"), MPT_USTRING("cc0")), cc0);
 		}
 		{
-			IniFileSettingsContainer conf{filename};
+			FileSettingsContainer<ImmediateWindowsIniFileSettingsBackend> conf{filename};
 			//VERIFY_EQUAL(conf.Read<mpt::ustring>(SettingPath(MPT_USTRING("Test"), MPT_USTRING("spacespacecharspacespace"))), spacespacecharspacespace);
 			//VERIFY_EQUAL(conf.Read<mpt::ustring>(SettingPath(MPT_USTRING("Test"), MPT_USTRING("threespaces"))), threespaces);
 			//VERIFY_EQUAL(conf.Read<mpt::ustring>(SettingPath(MPT_USTRING("Test"), MPT_USTRING("tab"))), tab);
@@ -3160,6 +3185,109 @@
 		DeleteFile(mpt::support_long_path(filename.AsNative()).c_str());
 	}
 
+	{
+		{
+			FileSettingsContainer<CachedIniFileSettingsBackend> conf{filename};
+			conf.Write<mpt::ustring>(SettingPath(MPT_USTRING("Test"), MPT_USTRING("spacespacecharspacespace")), spacespacecharspacespace);
+			conf.Write<mpt::ustring>(SettingPath(MPT_USTRING("Test"), MPT_USTRING("threespaces")), threespaces);
+			conf.Write<mpt::ustring>(SettingPath(MPT_USTRING("Test"), MPT_USTRING("tab")), tab);
+			conf.Write<mpt::ustring>(SettingPath(MPT_USTRING("Test"), MPT_USTRING("tokens")), tokens);
+			conf.Write<mpt::ustring>(SettingPath(MPT_USTRING("Test"), MPT_USTRING("xcrlfy")), xcrlfy);
+			conf.Write<mpt::ustring>(SettingPath(MPT_USTRING("Test"), MPT_USTRING("cc0")), cc0);
+		}
+		{
+			FileSettingsContainer<CachedIniFileSettingsBackend> conf{filename};
+			VERIFY_EQUAL(conf.Read<mpt::ustring>(SettingPath(MPT_USTRING("Test"), MPT_USTRING("spacespacecharspacespace"))), spacespacecharspacespace);
+			VERIFY_EQUAL(conf.Read<mpt::ustring>(SettingPath(MPT_USTRING("Test"), MPT_USTRING("threespaces"))), threespaces);
+			VERIFY_EQUAL(conf.Read<mpt::ustring>(SettingPath(MPT_USTRING("Test"), MPT_USTRING("tab"))), tab);
+			VERIFY_EQUAL(conf.Read<mpt::ustring>(SettingPath(MPT_USTRING("Test"), MPT_USTRING("tokens"))), tokens);
+			VERIFY_EQUAL(conf.Read<mpt::ustring>(SettingPath(MPT_USTRING("Test"), MPT_USTRING("xcrlfy"))), xcrlfy);
+			VERIFY_EQUAL(conf.Read<mpt::ustring>(SettingPath(MPT_USTRING("Test"), MPT_USTRING("cc0"))), cc0);
+		}
+		DeleteFile(mpt::support_long_path(filename.AsNative()).c_str());
+	}
+
+	// escaping
+	{
+		DeleteFile(mpt::support_long_path(filename.AsNative()).c_str());
+		{
+			mpt::IO::SafeOutputFile outputfile{filename, std::ios::binary};
+			mpt::IO::ofstream & outputstream = outputfile.stream();
+			mpt::IO::WriteTextCRLF(outputstream, "[Test]");
+			mpt::IO::WriteTextCRLF(outputstream, mpt::ToCharset(mpt::Charset::UTF8, U_("Foo1 = ^")));
+			mpt::IO::WriteTextCRLF(outputstream, mpt::ToCharset(mpt::Charset::UTF8, U_("Foo2 = ^^")));
+			mpt::IO::WriteTextCRLF(outputstream, mpt::ToCharset(mpt::Charset::UTF8, U_("Foo3 = ^^^")));
+		}
+		{
+			FileSettingsContainer<ImmediateWindowsIniFileSettingsBackend> inifile{filename};
+			VERIFY_EQUAL(inifile.Read<mpt::ustring>(SettingPath{U_("Test"), U_("Foo1")}, U_("")), U_("^"));
+			VERIFY_EQUAL(inifile.Read<mpt::ustring>(SettingPath{U_("Test"), U_("Foo2")}, U_("")), U_("^^"));
+			VERIFY_EQUAL(inifile.Read<mpt::ustring>(SettingPath{U_("Test"), U_("Foo3")}, U_("")), U_("^^^"));
+		}
+		DeleteFile(mpt::support_long_path(filename.AsNative()).c_str());
+	}
+	{
+		DeleteFile(mpt::support_long_path(filename.AsNative()).c_str());
+		{
+			mpt::IO::SafeOutputFile outputfile{filename, std::ios::binary};
+			mpt::IO::ofstream & outputstream = outputfile.stream();
+			mpt::IO::WriteTextCRLF(outputstream, "[Test]");
+			mpt::IO::WriteTextCRLF(outputstream, mpt::ToCharset(mpt::Charset::UTF8, U_("Foo1 = ^")));
+			mpt::IO::WriteTextCRLF(outputstream, mpt::ToCharset(mpt::Charset::UTF8, U_("Foo2 = ^^")));
+			mpt::IO::WriteTextCRLF(outputstream, mpt::ToCharset(mpt::Charset::UTF8, U_("Foo3 = ^^^")));
+		}
+		{
+			FileSettingsContainer<CachedIniFileSettingsBackend> inifile{filename};
+			VERIFY_EQUAL(inifile.Read<mpt::ustring>(SettingPath{U_("Test"), U_("Foo1")}, U_("")), U_("^"));
+			VERIFY_EQUAL(inifile.Read<mpt::ustring>(SettingPath{U_("Test"), U_("Foo2")}, U_("")), U_("^^"));
+			VERIFY_EQUAL(inifile.Read<mpt::ustring>(SettingPath{U_("Test"), U_("Foo3")}, U_("")), U_("^^^"));
+		}
+		DeleteFile(mpt::support_long_path(filename.AsNative()).c_str());
+	}
+	{
+		DeleteFile(mpt::support_long_path(filename.AsNative()).c_str());
+		{
+			mpt::IO::SafeOutputFile outputfile{filename, std::ios::binary};
+			mpt::IO::ofstream & outputstream = outputfile.stream();
+			mpt::IO::WriteTextCRLF(outputstream, "[!Type]");
+			mpt::IO::WriteTextCRLF(outputstream, "!Format=org.openmpt.fileformat.ini");
+			mpt::IO::WriteTextCRLF(outputstream, "!VersionMajor=4");
+			mpt::IO::WriteTextCRLF(outputstream, "!VersionMinor=0");
+			mpt::IO::WriteTextCRLF(outputstream, "!VersionPatch=0");
+			mpt::IO::WriteTextCRLF(outputstream, "[Test]");
+			mpt::IO::WriteTextCRLF(outputstream, mpt::ToCharset(mpt::Charset::UTF8, U_("Foo1 = ^")));
+			mpt::IO::WriteTextCRLF(outputstream, mpt::ToCharset(mpt::Charset::UTF8, U_("Foo2 = ^^")));
+			mpt::IO::WriteTextCRLF(outputstream, mpt::ToCharset(mpt::Charset::UTF8, U_("Foo3 = ^^^")));
+		}
+		{
+			FileSettingsContainer<CachedIniFileSettingsBackend> inifile{filename};
+			VERIFY_EQUAL(inifile.Read<mpt::ustring>(SettingPath{U_("Test"), U_("Foo1")}, U_("")), U_("^"));
+			VERIFY_EQUAL(inifile.Read<mpt::ustring>(SettingPath{U_("Test"), U_("Foo2")}, U_("")), U_("^"));
+			VERIFY_EQUAL(inifile.Read<mpt::ustring>(SettingPath{U_("Test"), U_("Foo3")}, U_("")), U_("^^"));
+		}
+		DeleteFile(mpt::support_long_path(filename.AsNative()).c_str());
+	}
+
+	// casing
+	#if 0
+	{
+		{	
+			FileSettingsContainer<ImmediateWindowsIniFileSettingsBackend> inifile{filename};
+			inifile.Write<mpt::ustring>(SettingPath{U_("Test"), U_("foo")}, U_("a"));
+		}
+		{	
+			FileSettingsContainer<ImmediateWindowsIniFileSettingsBackend> inifile{filename};
+			inifile.Read<mpt::ustring>(SettingPath{U_("Test"), U_("Foo")}, U_("b"));
+			inifile.Write<mpt::ustring>(SettingPath{U_("Test"), U_("Foo")}, U_("c"));
+		}
+		{	
+			FileSettingsContainer<CachedIniFileSettingsBackend> inifile{filename};
+			VERIFY_EQUAL(inifile.Read<mpt::ustring>(SettingPath{U_("Test"), U_("Foo")}, U_("")), U_("c"));
+			VERIFY_EQUAL(inifile.Read<mpt::ustring>(SettingPath{U_("Test"), U_("foo")}, U_("")), U_(""));
+		}
+	}
+	#endif
+
 #endif // MODPLUG_TRACKER
 
 }
