Skip to content

Improve performance of map thumbnail loading#985

Merged
SadPencil merged 13 commits into
CnCNet:developfrom
11EJDE11:improve-map-thumbnail-loading-perf
Apr 26, 2026
Merged

Improve performance of map thumbnail loading#985
SadPencil merged 13 commits into
CnCNet:developfrom
11EJDE11:improve-map-thumbnail-loading-perf

Conversation

@11EJDE11
Copy link
Copy Markdown
Member

Replace the IniFile based preview loading with a single-pass line scanner that collects only [Preview] Size and [PreviewPack] values, avoiding the overhead of parsing and allocating unrelated sections.

Tested on ~2500 maps without issue. Speed is improved greatly - maps that would take a second or two before are now <100ms.

If we'd prefer not to reinvent INI loading and want to stick with Rampastring.Tools instead, then we can look at making some changes to the INI loading by adding a dictionary. It speeds it up, although not as good as this and I wasn't a huge fan of the methodology.

@github-actions
Copy link
Copy Markdown

Nightly build for this pull request:

  • artifacts.zip
    This comment is automatic and is meant to allow guests to get latest automatic builds without registering. It is updated on every successful build.

@Metadorius
Copy link
Copy Markdown
Member

If we'd prefer not to reinvent INI loading and want to stick with Rampastring.Tools instead, then we can look at making some changes to the INI loading by adding a dictionary. It speeds it up, although not as good as this and I wasn't a huge fan of the methodology.

I think there was a way to specify the specific subset of sections and keys to parse already, was it not used in this case?

@Rampastring
Copy link
Copy Markdown
Member

Rampastring commented Apr 18, 2026

@Metadorius It is partially used here. Not in this class, but where the preview extractor is called in Map.cs. It could be optimized to be slightly tighter, but it is unlikely that it'd be as fast as the custom reader.

@SadPencil
Copy link
Copy Markdown
Member

SadPencil commented Apr 19, 2026

I am thinking preserving the old code (slow, good readability) as a virtual class method, and let the new code (fast, bad readability) act as a class method that override it. In this way we can retain the old code as a "reference implementation"

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR speeds up non-immediate map thumbnail extraction by avoiding full INI parsing and instead scanning the map file once to read only [Preview] Size and [PreviewPack] values.

Changes:

  • Added FastMapPreviewExtractor that scans map files line-by-line and extracts preview payload + size.
  • Introduced IMapPreviewExtractor and refactored MapPreviewExtractor into an instantiable base class to share decompress/bitmap creation logic.
  • Switched Map.GetNonImmediatePreviewImage() to use the fast extractor.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs Refactored to instance-based API and exposed core helpers for reuse by the fast extractor.
DXMainClient/Domain/Multiplayer/Map.cs Uses FastMapPreviewExtractor for non-immediate preview extraction.
DXMainClient/Domain/Multiplayer/IMapPreviewExtractor.cs New interface for preview extraction.
DXMainClient/Domain/Multiplayer/FastMapPreviewExtractor.cs New single-pass scanner implementation for preview extraction.
Comments suppressed due to low confidence (1)

DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs:116

  • DecompressPreviewData creates an LzoStream inside a loop but never disposes it. This can retain buffers and file/stream resources longer than needed. Wrap the LzoStream (and underlying MemoryStream, if appropriate) in a using statement (or using var) to ensure timely disposal.
        protected byte[] DecompressPreviewData(byte[] dataSource, int decompressedDataSize, out string errorMessage)
        {
            try
            {
                byte[] dataDest = new byte[decompressedDataSize];

Comment thread DXMainClient/Domain/Multiplayer/FastMapPreviewExtractor.cs
Comment thread DXMainClient/Domain/Multiplayer/FastMapPreviewExtractor.cs
Comment thread DXMainClient/Domain/Multiplayer/FastMapPreviewExtractor.cs Outdated
Comment thread DXMainClient/Domain/Multiplayer/IMapPreviewExtractor.cs Outdated
Comment thread DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

Comment thread DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs
Comment thread DXMainClient/Domain/Multiplayer/Map.cs
Comment thread DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs
Comment thread DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs:137

  • DecompressPreviewData declares a non-null byte[] return, but it returns null on error paths. Since this method is now protected and consumed by nullable-enabled code, the non-null contract is misleading. Update the return type (and errorMessage out parameter) to reflect nullability, or add a nullable-enabled region for these members.
        protected byte[] DecompressPreviewData(byte[] dataSource, int decompressedDataSize, out string errorMessage)
        {
            try
            {
                byte[] dataDest = new byte[decompressedDataSize];
                int readBytes = 0, writtenBytes = 0;

                while (true)
                {
                    if (readBytes >= dataSource.Length)
                        break;

                    ushort sizeCompressed = BinaryPrimitives.ReadUInt16LittleEndian(dataSource.AsSpan(readBytes));
                    readBytes += 2;
                    ushort sizeUncompressed = BinaryPrimitives.ReadUInt16LittleEndian(dataSource.AsSpan(readBytes));
                    readBytes += 2;

                    if (sizeCompressed == 0 || sizeUncompressed == 0)
                        break;

                    if (readBytes + sizeCompressed > dataSource.Length ||
                        writtenBytes + sizeUncompressed > dataDest.Length)
                    {
                        errorMessage = "Preview data does not match preview size or the data is corrupted, unable to extract preview.";
                        return null;
                    }

DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs:172

  • CreatePreviewBitmapFromImageData declares a non-null Image return, but it returns null on validation/errors. With FastMapPreviewExtractor calling this from a nullable-enabled file, the signature is misleading and can mask null-safety issues. Change the return type (and errorMessage out parameter) to nullable, or add a nullable-enabled region for these members.
        protected Image CreatePreviewBitmapFromImageData(int width, int height, byte[] imageData, out string errorMessage)
        {
            const int pixelFormatBitCount = 24;
            const int pixelFormatByteCount = pixelFormatBitCount / 8;

            if (imageData.Length != width * height * pixelFormatByteCount)
            {
                errorMessage = "Provided preview image dimensions do not match preview image data length.";
                return null;
            }

@SadPencil
Copy link
Copy Markdown
Member

SadPencil commented Apr 26, 2026

Copilot was right. We did have maps that mark a hidden map preview but the key "1" is not in the first place

#985 (comment)

图片

@SadPencil SadPencil merged commit 231b9b5 into CnCNet:develop Apr 26, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants