From 3949dcfce95af4bd305f258ff5b170d7d50435f6 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Sun, 12 Apr 2026 21:04:15 -0500 Subject: VFS perf fixes, icon/stylesheet compat, download filename fix VFS: - Open backing fd at mo2_open time for read-only handles so mo2_read can splice without re-opening the file on every read call. - Bump RLIMIT_NOFILE at mount time to fit the resulting fd pressure when the game keeps hundreds of BSAs open. - Dedicated node_cache_mutex so resolveByInode / mo2_lookup stop racing unordered_map writes while multiple tree_mutex readers are active. - max_read=1MB + matching conn->max_read and raised max_readahead/max_write so the kernel merges Wine's small sequential reads into bigger FUSE requests. - Per-op wall-clock counters in mo2_init() logs so we can distinguish VFS latency from game-side work. Proton launch: - Write dxvk.conf to the prefix and set DXVK_CONFIG_FILE to force dxvk.enableGraphicsPipelineLibrary=False, avoiding long GPL compile stalls on first run. UI / plugin compat: - Clamp IconDelegate's per-icon width to [8, 16] so narrow content columns don't trigger QIcon::pixmap(0,0) returning null and logging "failed to load icon" every repaint. - Pre-create lowercase symlinks in the stylesheet directory so QSS files authored on Windows resolve url(foo.svg) when on-disk files are Foo.svg. - Flip content icons empty-chessboard.png and facegen.png to 8-bit RGBA so Qt's built-in PNG reader loads them uniformly. - Add mobase.Version.canonicalString shim for legacy Python plugins that still call the old VersionInfo method on appVersion()'s return. - BSPluginList: compare normalized plugin name lists instead of byte hashes so the post-run case-fix refresh stops spuriously firing the "load order changed" dialog. - BSPluginList disabled by default. Download manager: - Parse CDN URL with QUrl + QUrlQuery and read the filename from the response-content-disposition query param (RFC 6266 quoted / unquoted / ext-value forms), not QFileInfo on the raw URL. - When the API or URL gives us a CDN object key (no archive extension), prefer the actual Content-Disposition header from the live response in metaDataChanged and downloadFinished. Co-Authored-By: Claude Opus 4.6 (1M context) --- libs/installer_bsplugins/src/TESFile/Stream.h | 291 ++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 libs/installer_bsplugins/src/TESFile/Stream.h (limited to 'libs/installer_bsplugins/src/TESFile/Stream.h') diff --git a/libs/installer_bsplugins/src/TESFile/Stream.h b/libs/installer_bsplugins/src/TESFile/Stream.h new file mode 100644 index 0000000..db22580 --- /dev/null +++ b/libs/installer_bsplugins/src/TESFile/Stream.h @@ -0,0 +1,291 @@ +#ifndef TESFILE_STREAM_H +#define TESFILE_STREAM_H + +#include "Type.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace TESFile +{ + +template +inline T readType(std::istream& stream) +{ + union + { + char buffer[sizeof(T)]; + T value; + }; + std::memset(buffer, 0x42, sizeof(T)); + stream.read(buffer, sizeof(T)); + return value; +} + +inline std::string readZstring(std::istream& stream) +{ + std::string value; + std::getline(stream, value, '\0'); + return value; +} + +inline int icompare(const std::string& a, const std::string& b) +{ + const std::size_t n = std::min(a.size(), b.size()); + for (std::size_t i = 0; i < n; ++i) { + const unsigned char ca = + static_cast(std::tolower(static_cast(a[i]))); + const unsigned char cb = + static_cast(std::tolower(static_cast(b[i]))); + if (ca != cb) + return ca < cb ? -1 : 1; + } + if (a.size() == b.size()) + return 0; + return a.size() < b.size() ? -1 : 1; +} + +inline bool iequals(const std::string& a, const std::string& b) +{ + return icompare(a, b) == 0; +} + +template +inline auto find(R&& r, const std::string& value, Proj proj = {}) +{ + return std::ranges::find_if( + r, + [&](auto&& val) { + return iequals(val, value); + }, + proj); +} + +struct less +{ + bool operator()(const std::string& a, const std::string& b) const + { + return icompare(a, b) < 0; + } +}; + +enum class TESFormat +{ + Standard, + Oblivion, + Morrowind, +}; + +enum class GroupType : std::int32_t +{ + Top, + WorldChildren, + InteriorCellBlock, + InteriorCellSubBlock, + ExteriorCellBlock, + ExteriorCellSubBlock, + CellChildren, + TopicChildren, + CellPersistentChildren, + CellTemporaryChildren, + QuestChildren, + CellVisibleDistantChildren = 10, +}; + +struct RecordFlags +{ + enum TES4Flag : std::uint32_t + { + Master = 0x1, + Optimized = 0x10, + Localized = 0x80, + SmallNew = 0x100, + SmallOld = 0x200, + Update = 0x200, + Medium = 0x400, + Blueprint = 0x800, + }; + + enum Flag : std::uint32_t + { + Compressed = 0x40000, + }; +}; + +struct RecordHeader +{ + Type type; + std::uint32_t dataSize; + union + { + struct + { + std::uint32_t flags; + std::uint32_t formId; + } formData; + struct + { + std::uint32_t label; + GroupType groupType; + } groupData; + }; + union + { + struct + { + std::uint32_t revision; + Type firstChunk; + } old; + struct + { + std::uint16_t timestamp; + std::uint16_t revision; + std::uint16_t version; + std::uint16_t unknown1; + }; + }; +}; +static_assert(sizeof(RecordHeader) == 24); + +struct ChunkHeader +{ + Type type; + std::uint16_t dataSize; +}; +static_assert(sizeof(ChunkHeader) == 6); + +class GroupData final +{ +public: + constexpr GroupData() : formId_{0}, groupType_{GroupType::Top} {} + + constexpr GroupData(std::uint32_t label, GroupType type) + : formId_{label}, groupType_{type} + {} + + constexpr auto operator<=>(const GroupData& other) const + { + if (groupType_ != other.groupType_) { + return groupType_ <=> other.groupType_; + } else { + if (hasFormType()) { + return formType() <=> other.formType(); + } else if (hasBlock()) { + return block() <=> other.block(); + } else if (hasGridCell()) { + return gridCell() <=> other.gridCell(); + } else { + return formId_ <=> other.formId_; + } + } + } + + constexpr bool operator==(const GroupData& other) const + { + return groupType_ == other.groupType_ && formId_ == other.formId_; + } + + [[nodiscard]] constexpr GroupType type() const { return groupType_; } + + [[nodiscard]] constexpr bool hasFormType() const + { + return groupType_ == GroupType::Top; + } + + [[nodiscard]] constexpr bool hasParent() const + { + return groupType_ == GroupType::WorldChildren || + groupType_ == GroupType::CellChildren || + groupType_ == GroupType::TopicChildren || + groupType_ == GroupType::CellPersistentChildren || + groupType_ == GroupType::CellTemporaryChildren || + groupType_ == GroupType::QuestChildren; + } + + [[nodiscard]] constexpr bool hasDirectParent() const + { + return groupType_ == GroupType::WorldChildren || + groupType_ == GroupType::CellChildren || + groupType_ == GroupType::TopicChildren || + groupType_ == GroupType::QuestChildren; + } + + [[nodiscard]] constexpr bool hasBlock() const + { + return groupType_ == GroupType::InteriorCellBlock || + groupType_ == GroupType::InteriorCellSubBlock; + } + + [[nodiscard]] constexpr bool hasGridCell() const + { + return groupType_ == GroupType::ExteriorCellBlock || + groupType_ == GroupType::ExteriorCellSubBlock; + } + + [[nodiscard]] constexpr Type formType() const { return formType_; } + + [[nodiscard]] constexpr std::uint32_t parent() const { return formId_; } + + [[nodiscard]] constexpr std::int32_t block() const { return number_; } + + [[nodiscard]] constexpr std::pair gridCell() const + { + return {cell_.x, cell_.y}; + } + + constexpr void setLocalIndex(std::uint8_t index) + { + if (hasParent()) { + formId_ = (formId_ & 0xFFFFFFU) | (index << 24U); + } + } + +private: + GroupType groupType_; + union + { + Type formType_; + std::uint32_t formId_; + std::int32_t number_; + struct + { + std::int16_t y; + std::int16_t x; + } cell_; + }; +}; + +class FormData final +{ +public: + constexpr FormData(Type type, std::uint32_t flags, std::uint32_t formId, std::uint16_t formVersion) + : type_{type}, flags_{flags}, formId_{formId}, formVersion_{formVersion} + {} + + [[nodiscard]] constexpr Type type() const { return type_; } + + [[nodiscard]] constexpr std::uint32_t flags() const { return flags_; } + + [[nodiscard]] constexpr std::uint32_t formId() const { return formId_; } + + [[nodiscard]] constexpr std::uint16_t formVersion() const { return formVersion_; } + + [[nodiscard]] constexpr std::uint8_t localModIndex() const { return formId_ >> 24; } + +private: + Type type_; + std::uint32_t flags_; + std::uint32_t formId_; + std::uint16_t formVersion_; +}; + +} // namespace TESFile + +using namespace TESFile::literals; + +#endif // TESFILE_STREAM_H -- cgit v1.3.1