grid-apps-cmms/docs/refs/MSLA_FORMAT_ADDENDUM.md
2026-07-26 13:52:55 -04:00

13 KiB

mSLA Format Reference - Critical Addendum

CTB RLE & PM4N Version Corrections

Date: 2026-05-16 Status: Code-verified corrections to v2 reference


Issue 1: CTB RLE Encoding - CORRECTED

Previous (Incorrect) Description

"RLE125: Simple 2-byte fixed format (color, length), max 125 pixels"

Actual Implementation (from ChituboxFile.cs:825-908)

CTB uses a 7-bit grayscale variable-length RLE with optional encryption, NOT simple RLE125.

Encoding Algorithm

Step 1: Color Quantization

grey7 = pixel_value >> 1  // Convert 8-bit (0-255) to 7-bit (0-127)

Step 2: Variable-Length Run Encoding

The encoding uses a variable-length stride format:

Stride Length Bytes Format Description
1 pixel 1 byte [0ccccccc] Color only, bit 7 clear
2-127 pixels 2 bytes [1ccccccc] [0rrrrrrr] Color + 7-bit length
128-16383 pixels 3 bytes [1ccccccc] [10rrrrrr] [rrrrrrrr] Color + 14-bit length
16384-2097151 pixels 4 bytes [1ccccccc] [110rrrrr] [rrrrrrrr] [rrrrrrrr] Color + 21-bit length
2097152-268435455 pixels 5 bytes [1ccccccc] [1110rrrr] [rrrrrrrr] [rrrrrrrr] [rrrrrrrr] Color + 28-bit length

Bit Layout:

  • c = color bits (7-bit grayscale, 0-127)
  • r = repeat/stride bits
  • Bit 7 of first byte: 1 if stride > 1, 0 if stride == 1
  • Length byte prefix bits indicate multi-byte length encoding

Detailed Encoding Logic (from source)

void AddRep()
{
    if (stride == 0) return;

    if (stride > 1) {
        color |= 0x80;  // Set bit 7 to indicate run follows
    }
    rawData.Add(color);

    if (stride <= 1) {
        // Single pixel: just the color byte
        return;
    }

    if (stride <= 0x7f) {  // 127
        // 1-byte length: [0rrrrrrr]
        rawData.Add((byte)stride);
        return;
    }

    if (stride <= 0x3fff) {  // 16383
        // 2-byte length: [10rrrrrr] [rrrrrrrr]
        rawData.Add((byte)((stride >> 8) | 0x80));
        rawData.Add((byte)stride);
        return;
    }

    if (stride <= 0x1fffff) {  // 2097151
        // 3-byte length: [110rrrrr] [rrrrrrrr] [rrrrrrrr]
        rawData.Add((byte)((stride >> 16) | 0xc0));
        rawData.Add((byte)(stride >> 8));
        rawData.Add((byte)stride);
        return;
    }

    if (stride <= 0xfffffff) {  // 268435455
        // 4-byte length: [1110rrrr] [rrrrrrrr] [rrrrrrrr] [rrrrrrrr]
        rawData.Add((byte)((stride >> 24) | 0xe0));
        rawData.Add((byte)(stride >> 16));
        rawData.Add((byte)(stride >> 8));
        rawData.Add((byte)stride);
    }
}

Decoding Algorithm

def decode_ctb_rle(data: bytes, width: int, height: int) -> bytes:
    pixels = bytearray(width * height)
    pixel_pos = 0
    i = 0

    while i < len(data):
        color_byte = data[i]
        i += 1

        # Extract 7-bit color
        color = (color_byte & 0x7F) << 1  # Expand back to 8-bit

        # Check if run follows (bit 7 set)
        if (color_byte & 0x80) == 0:
            # Single pixel
            pixels[pixel_pos] = color
            pixel_pos += 1
            continue

        # Multi-pixel run: decode length
        length_byte = data[i]
        i += 1

        if (length_byte & 0x80) == 0:
            # 1-byte length (7 bits)
            stride = length_byte
        elif (length_byte & 0x40) == 0:
            # 2-byte length (14 bits)
            stride = ((length_byte & 0x3F) << 8) | data[i]
            i += 1
        elif (length_byte & 0x20) == 0:
            # 3-byte length (21 bits)
            stride = ((length_byte & 0x1F) << 16) | (data[i] << 8) | data[i+1]
            i += 2
        else:
            # 4-byte length (28 bits)
            stride = ((length_byte & 0x0F) << 24) | (data[i] << 16) | (data[i+1] << 8) | data[i+2]
            i += 3

        # Fill pixels
        for _ in range(stride):
            pixels[pixel_pos] = color
            pixel_pos += 1

    return pixels

Encryption (Optional)

If HeaderSettings.EncryptionKey > 0, RLE data is XORed with a per-layer key:

Encryption Algorithm (from ChituboxFile.cs:2227-2248):

void LayerRleCryptBuffer(uint seed, uint layerIndex, byte[] input)
{
    if (seed == 0) return;

    uint init = seed * 0x2d83cdac + 0xd8a83423;
    uint key = (layerIndex * 0x1e1530cd + 0xec3d47cd) * init;

    int index = 0;
    for (int i = 0; i < input.Length; i++)
    {
        byte k = (byte)(key >> (8 * index));
        index++;

        if ((index & 3) == 0) {
            key += init;
            index = 0;
        }

        input[i] = (byte)(input[i] ^ k);
    }
}

Key Generation:

init = seed * 0x2D83CDAC + 0xD8A83423
key_base = (layerIndex * 0x1E1530CD + 0xEC3D47CD) * init

For each 4-byte block:
  XOR with bytes from key_base (little-endian)
  After 4 bytes, update: key_base += init

Anti-Aliasing Support

CTB format supports 7-bit grayscale (128 levels), enabling smooth anti-aliasing:

  • 0x00 = fully masked (black)
  • 0x7F = fully exposed (white)
  • 0x01-0x7E = partial exposure (anti-aliasing)

Example Encodings

Example 1: Single white pixel

Input: 1 pixel at value 255
Encoded: 7F
  - grey7 = 255 >> 1 = 127 = 0x7F
  - stride = 1, so bit 7 stays clear
  - Output: [0x7F] (1 byte)

Example 2: Run of 100 white pixels

Input: 100 pixels at value 255
Encoded: FF 64
  - grey7 = 127 = 0x7F
  - stride = 100 = 0x64
  - 100 <= 127, so 1-byte length
  - color with bit 7 set: 0x7F | 0x80 = 0xFF
  - Output: [0xFF] [0x64] (2 bytes)

Example 3: Run of 500 gray pixels (value 128)

Input: 500 pixels at value 128
Encoded: C0 81 F4
  - grey7 = 128 >> 1 = 64 = 0x40
  - stride = 500 = 0x1F4
  - 500 > 127 and <= 16383, so 2-byte length
  - color with bit 7: 0x40 | 0x80 = 0xC0
  - length high: (500 >> 8) | 0x80 = 0x01 | 0x80 = 0x81
  - length low: 500 & 0xFF = 0xF4
  - Output: [0xC0] [0x81] [0xF4] (3 bytes)

Example 4: Run of 20000 pixels

Input: 20000 pixels at value 200
Encoded: E4 C4 E2 20
  - grey7 = 200 >> 1 = 100 = 0x64
  - stride = 20000 = 0x4E20
  - 20000 > 16383, so 3-byte length
  - color: 0x64 | 0x80 = 0xE4
  - length encoding: 0x4E20 with 0xC0 prefix
    - byte 0: (0x4E20 >> 16) | 0xC0 = 0x00 | 0xC0 = 0xC0
    - Wait, let me recalculate...
    - Actually 20000 = 0x4E20
    - High nibble: 0x4, middle: 0xE, low: 0x20
    - Prefix: ((0x4E20 >> 16) & 0x1F) | 0xC0 = 0x00 | 0xC0 = 0xC0
    - Byte 1: 0x4E20 >> 8 = 0x4E
    - Byte 2: 0x4E20 & 0xFF = 0x20
  - Output: [0xE4] [0xC0] [0x4E] [0x20] (4 bytes)

Key Differences from Simple RLE125

Feature Simple RLE125 CTB Variable RLE
Color depth 8-bit (0-255) 7-bit (0-127)
Max run (1-byte) 125 127
Max run (total) 125 268,435,455
Bytes per run 2 (fixed) 1-5 (variable)
Single pixel 2 bytes 1 byte
Anti-aliasing Binary or limited Full 7-bit grayscale
Encryption No Optional per-layer XOR
Compression Moderate Better for long runs

Implementation Recommendations

  1. Decoding CTB:

    • Read color byte
    • Check bit 7 for run flag
    • If clear: single pixel
    • If set: decode variable-length stride
    • Apply encryption if seed != 0
  2. Encoding CTB:

    • Quantize to 7-bit: grey7 = pixel >> 1
    • Group consecutive identical pixels
    • Encode with optimal byte count (1-5 bytes)
    • Apply encryption if required
  3. Encryption Handling:

    • Always check HeaderSettings.EncryptionKey
    • If non-zero, decrypt before RLE decode
    • Use layer-specific key derivation

Issue 2: .pm4n Version Classification - AMBIGUOUS

Current Status in UVtools

Extension Registration:

// AnycubicFile.cs:1058
new(typeof(AnycubicFile), "pm4n", "Photon Mono 4 (PM4N)")

Machine Mapping:

// AnycubicFile.cs:1731-1733
if (FileEndsWith(".pm4n"))
{
    return AnyCubicMachine.PhotonMono4;
}

Version Mapping:

// AnycubicFile.cs:1155-1191
public override uint[] GetAvailableVersionsForExtension(string? extension)
{
    switch (extension)
    {
        case "pm3n":
        case "pm5":
        case "px6s":
            return [VERSION_517];        // ← pm3n, pm5, px6s are v517

        case "pm5s":
        case "m5sp":
            return [VERSION_518];        // ← pm5s, m5sp are v518

        // NOTE: .pm4n is NOT in the switch statement!

        default:
            return AvailableVersions;    // ← .pm4n falls through here
    }
}

// AvailableVersions = [VERSION_1, VERSION_515, VERSION_516, VERSION_517, VERSION_518]

The Ambiguity

.pm4n extension:

  • ✓ Registered as valid extension
  • ✓ Maps to PhotonMono4 machine
  • NOT explicitly assigned to any version in GetAvailableVersionsForExtension
  • Returns all versions: [1, 515, 516, 517, 518]

Possible Interpretations

  1. Oversight: UVtools forgot to add .pm4n to the v517 case block
  2. Intentional: .pm4n can legitimately be multiple versions (firmware evolution)
  3. Unknown: Insufficient real-world fixtures to determine correct version

User Implementation Note

User's current implementation treats .pm4n as v517, which is:

  • ✓ Consistent with similar naming (.pm3n, .pm5 are v517)
  • ✓ Logical based on product generation
  • ⚠️ UNVERIFIED by UVtools code (could be any version)

Recommendations

For Implementation:

// Conservative approach (allow multiple versions):
if (extension == "pm4n") {
    return [VERSION_515, VERSION_516, VERSION_517];
}

// Aggressive approach (assume v517 based on naming):
if (extension == "pm4n") {
    return [VERSION_517];
}

For Validation:

  1. Obtain real .pm4n fixture files from PhotonMono4 printer
  2. Decode header and check FileMarkSettings.Version value
  3. Verify layer table structure (92 bytes = v517, 96 bytes = v518)
  4. Document actual version found in fixtures

Fixture Verification Checklist:

  • Acquire .pm4n file from PhotonMono4
  • Read offset 0x02 (uint16 Version field)
  • Check layer table entry size
  • Verify preview count (2 = v515/516, 7 = v517, 9 = v518)
  • Document findings

Comparison with Similar Extensions

Extension Machine Version in Code Logic
.pm3n PhotonMono2 517 (explicit) ✓ Defined
.pm4n PhotonMono4 ??? (default all) ⚠️ Ambiguous
.pm5 PhotonMonoM5 517 (explicit) ✓ Defined
.pm5s PhotonMonoM5s 518 (explicit) ✓ Defined

Pattern suggests: .pm4n should likely be v517, but this is NOT verified in UVtools source.


Updated Classification

Anycubic Modern Formats (CORRECTED)

Extension Machine Version Status
.pm3n PhotonMono2 517 ✓ Verified
.pm4n PhotonMono4 517 (assumed) ⚠️ UNVERIFIED
.pm5 PhotonMonoM5 517 ✓ Verified
.px6s PhotonMonoX6Ks 517 ✓ Verified
.pm5s PhotonMonoM5s 518 ✓ Verified
.m5sp PhotonMonoM5sPro 518 ✓ Verified

Action Items for Documentation

  1. CTB RLE: Update main reference with variable-length encoding details
  2. .pm4n: Mark as "Assumed v517, requires fixture validation"
  3. Testing: Add note about lack of test fixtures in UVtools repository
  4. Encryption: Document optional per-layer XOR encryption for CTB

Code References

CTB RLE Encoding:

  • Implementation: ChituboxFile.cs:825-908 (EncodeCtbImage)
  • Decoding: ChituboxFile.cs:685-745 (DecodeImage)
  • Encryption: ChituboxFile.cs:2220-2248 (LayerRleCrypt)

Anycubic Versions:

  • Extension mapping: AnycubicFile.cs:1155-1191 (GetAvailableVersionsForExtension)
  • Machine detection: AnycubicFile.cs:1731-1733 (FileEndsWith check)
  • Version constants: AnycubicFile.cs:30-33 (VERSION_515-518)

Summary

CTB RLE:

  • Previous: Simple RLE125 (2 bytes/run, max 125)
  • Correct: Variable-length 7-bit grayscale RLE (1-5 bytes/run, max 268M, optional encryption)

.pm4n Version:

  • Previous: Listed under v517 without caveat
  • Correct: Ambiguous in UVtools (defaults to all versions), assumed v517 pending fixture validation

Document Status: Ready for integration into main reference with caveats noted.