Skip to content
This repository was archived by the owner on Jan 30, 2026. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,36 @@
"version": "5.12.0",
"commands": [
"dotnet-gitversion"
]
],
"rollForward": false
},
"thirdlicense": {
"version": "1.3.1",
"commands": [
"thirdlicense"
]
],
"rollForward": false
},
"dotnet-reportgenerator-globaltool": {
"version": "5.2.0",
"commands": [
"reportgenerator"
]
],
"rollForward": false
},
"docfx": {
"version": "2.75.2",
"version": "2.77.0",
"commands": [
"docfx"
]
],
"rollForward": false
},
"gitreleasemanager.tool": {
"version": "0.16.0",
"commands": [
"dotnet-gitreleasemanager"
]
],
"rollForward": false
}
}
}
4 changes: 2 additions & 2 deletions .github/workflows/build-and-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
name: "Build"
uses: ./.github/workflows/build.yml
with:
dotnet_version: '8.0.204'
dotnet_version: '8.0.401'
secrets:
test_resources: ${{ secrets.TEST_RESOURCES_URI_V1 }}

Expand All @@ -27,7 +27,7 @@ jobs:
needs: build
uses: ./.github/workflows/deploy.yml
with:
dotnet_version: '8.0.204'
dotnet_version: '8.0.401'
azure_nuget_feed: 'https://pkgs.dev.azure.com/SceneGate/SceneGate/_packaging/SceneGate-Preview/nuget/v3/index.json'
secrets:
nuget_preview_token: "az" # Dummy values as we use Azure DevOps onlyg
Expand Down
52 changes: 52 additions & 0 deletions docs/articles/specs/compression/lzss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# LZSS

[Lempel–Ziv–Storer–Szymanski (LZSS)](https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Storer%E2%80%93Szymanski)
is a lossless compression algorithm implemented in the BIOS of the GBA and DS.
Software can trigger the decompression functions via
[SWI calls](https://problemkaputt.de/gbatek.htm#biosdecompressionfunctions).

## Format

The GBA/DS BIOS expects a 32-bits header before the compression data.

| Offset | Type | Description |
| ------ | ------ | --------------- |
| 0x00 | uint | Header |
| 0x04 | byte[] | Compressed data |

The header bit fields are:

- Bits 0-3: reserved (0)
- Bits 4-7: compression type `1`
- Bits 8-31: decompressed length

### Compression format

The compression supports two operation modes:

- Copy the next byte from the input stream into the output stream.
- Repeat a sequence from the decompressed data in the output

The compressed data starts with a flag byte that indicates the mode for the next
8 operations. The bits are processed in big-endian order that is, from bit 7 to
bit 0.

If the next flag bit is 0, then the next byte from the input stream is written
into the output stream.

If the next flag bit is 1, then there is a 16-bits value in the input stream
containing the repeat information:

- Bits 0-11: backwards counting position of the start of the sequence in the
output stream.
- Bits 12-15: sequence length - 3 (minimum sequence length)

> [!NOTE]
> The length of the sequence could be larger than the available output at the
> start of the decoding. While repeating the sequence, we may need to copy also
> bytes that we just wrote. For instance, we could repeat the last two bytes of
> the output 5 times by encoding the position 1 and a length of 10.

After processing every flag bit, the next input byte contains the next flags.
The operation repeats until reaching the decompressed size or running out of
input data. Note that there may be some unused bits (set to 0).
4 changes: 4 additions & 0 deletions docs/articles/specs/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@
href: cartridge/program.md
- name: Security
href: cartridge/security.md

- name: 🗜️ Compression
- name: LZSS
href: compression/lzss.md
33 changes: 33 additions & 0 deletions src/Ekona.PerformanceTests/LzssEncoderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace SceneGate.Ekona.PerformanceTests;

using BenchmarkDotNet.Attributes;
using SceneGate.Ekona.Compression;

[MemoryDiagnoser]
public class LzssEncoderTests
{
private Stream inputStream = null!;
private Stream outputStream = null!;

[GlobalSetup]
public void SetUp()
{
var input = new byte[Length];
Random.Shared.NextBytes(input);
inputStream = new MemoryStream(input);

var output = new byte[Length * 2];
outputStream = new MemoryStream(output);
}

[Params(512, 10 * 1024, 3 * 1024 * 1024)]
public int Length { get; set; }

[Benchmark]
public Stream Encode()
{
outputStream.Position = 0;
var encoder = new LzssEncoder(outputStream);
return encoder.Convert(inputStream);
}
}
4 changes: 2 additions & 2 deletions src/Ekona.PerformanceTests/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using BenchmarkDotNet.Running;

namespace SceneGate.Ekona.PerformanceTests;

using BenchmarkDotNet.Running;

public static class Program
{
public static void Main(string[] args) =>
Expand Down
120 changes: 120 additions & 0 deletions src/Ekona.Tests/Compression/LzssDecoderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
namespace SceneGate.Ekona.Tests.Compression;

using System;
using System.IO;
using NUnit.Framework;
using SceneGate.Ekona.Compression;
using Yarhl.IO;

[TestFixture]
public class LzssDecoderTests
{
[Test]
public void DecodeRawToken()
{
byte[] input = [0x00, 0xBE, 0xB0, 0xCA, 0xFE, 0xC0, 0xC0, 0xBA, 0xBE];
byte[] expected = [0xBE, 0xB0, 0xCA, 0xFE, 0xC0, 0xC0, 0xBA, 0xBE];

AssertConversion(input, expected);
}

[Test]
public void DecodeCopyToken()
{
byte[] input = [
0b0001_0011,
0xBE, 0xEE, 0xEF,
0x0_0, 0x02, // full past
0xCA, 0xFE,
0x1_0, 0x01, // with future
0x0_0, 0x00, // full future
];
byte[] expected = [
0xBE, 0xEE, 0xEF, 0xBE, 0xEE, 0xEF,
0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE,
0xFE, 0xFE, 0xFE,
];

AssertConversion(input, expected);
}

[Test]
public void DecodeThrowsEOSWithMissingRawAfterReadingFlag()
{
using var input = new DataStream();
input.Write([ 0b0000_0000 ]);

using DataStream actual = new();
var decoder = new LzssDecoder(actual, false);
Assert.That(() => decoder.Convert(input), Throws.InstanceOf<EndOfStreamException>());
}

[Test]
public void DecodeStopWithPaddingFlagBits()
{
byte[] input = [ 0b0000_0000, 0xAA ];
byte[] expected = [ 0xAA ];

AssertConversion(input, expected);
}

[Test]
public void DecodeThrowsEOSWithMissingCopyInfo()
{
using DataStream input = new();
input.Write([ 0b0100_0000, 0xAA, 0x00 ]);

var decoder = new LzssDecoder(new DataStream(), false);
Assert.That(() => decoder.Convert(input), Throws.InstanceOf<EndOfStreamException>());
}

[Test]
public void DecodeNewFlagAfter8Tokens()
{
byte[] input = [0x00, 0xBE, 0xB0, 0xCA, 0xFE, 0xC0, 0xC0, 0xBA, 0xBE, 0x00, 0xAA];
byte[] expected = [0xBE, 0xB0, 0xCA, 0xFE, 0xC0, 0xC0, 0xBA, 0xBE, 0xAA];

AssertConversion(input, expected);
}

[Test]
public void ThrowWhenOutputBufferIsNotLargeEnough()
{
byte[] inputFailRaw = [ 0b0000_0000, 0xAA ];
byte[] inputFailCopy = [ 0b0100_0000, 0x00, 0x00, 0x00 ];

using var streamParent = new DataStream();
using var fixedOutputStream = new DataStream(streamParent, 0, 0);
Assert.That(
() => new LzssDecoder(fixedOutputStream, false).Convert(new MemoryStream(inputFailRaw)),
Throws.InstanceOf<InvalidOperationException>());
Assert.That(
() => new LzssDecoder(fixedOutputStream, false).Convert(new MemoryStream(inputFailCopy)),
Throws.InstanceOf<InvalidOperationException>());
}

[Test]
public void DecodeEmpty()
{
byte[] input = [];
byte[] expected = [];

AssertConversion(input, expected);
}

private void AssertConversion(byte[] input, byte[] expected)
{
using DataStream expectedStream = DataStreamFactory.FromArray(expected);
using DataStream inputStream = DataStreamFactory.FromArray(input);

using DataStream actual = new();
var decoder = new LzssDecoder(actual, false);
_ = decoder.Convert(inputStream);

Assert.Multiple(() => {
Assert.That(inputStream.Position, Is.EqualTo(input.Length));
Assert.That(actual.Length, Is.EqualTo(expected.Length));
Assert.That(expectedStream.Compare(actual), Is.True);
});
}
}
Loading