diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt
index 52e585e0ac..6bccb0845a 100644
--- a/Core/CMakeLists.txt
+++ b/Core/CMakeLists.txt
@@ -693,6 +693,8 @@ add_library(Core STATIC
Util/BlockAllocator.h
Util/PPGeDraw.cpp
Util/PPGeDraw.h
+ Util/KL4E.cpp
+ Util/KL4E.h
Util/PSARUnpack.cpp
Util/PSARUnpack.h
Util/RecentFiles.cpp
diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj
index f493c40f25..ebe55178a3 100644
--- a/Core/Core.vcxproj
+++ b/Core/Core.vcxproj
@@ -921,6 +921,7 @@
+
MaxSpeed
@@ -1289,6 +1290,7 @@
+
diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters
index c26c14f463..5829960173 100644
--- a/Core/Core.vcxproj.filters
+++ b/Core/Core.vcxproj.filters
@@ -363,6 +363,9 @@
Util
+
+ Util
+
Util
diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp
index 5ed7883ca1..e6c0603d54 100644
--- a/Core/HLE/sceKernelModule.cpp
+++ b/Core/HLE/sceKernelModule.cpp
@@ -48,6 +48,7 @@
#include "Core/ELF/ElfReader.h"
#include "Core/ELF/PBPReader.h"
#include "Core/ELF/PrxDecrypter.h"
+#include "Core/Util/KL4E.h"
#include "Core/FileSystems/FileSystem.h"
#include "Core/FileSystems/MetaFileSystem.h"
#include "Core/Util/BlockAllocator.h"
@@ -1273,7 +1274,7 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load
g_OSD.Show(OSDType::MESSAGE_WARNING, StringFromFormat("HLE for '%s' has been manually disabled", head->modname));
}
const u8 *in = ptr;
- const auto isGzip = head->comp_attribute & 1;
+ const bool isCompressed = (head->comp_attribute & 1) != 0;
// Kind of odd.
u32 size = head->psp_size;
if (size > elfSize) {
@@ -1311,22 +1312,36 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load
return nullptr;
}
- // decompress if required.
- if (isGzip) {
- _dbg_assert_(Read32(ptr + 0x150) != ELF_MAGIC);
-
+ // decompress if required. comp_attribute bit 0 says "compressed"; which scheme is then
+ // decided by the payload's own magic - gzip, or Sony's KL4E/KL3E. (JPCSP instead reads
+ // bits 8-11 of comp_attribute, but the magic is right there and can't disagree.)
+ if (isCompressed) {
// Can't decompress in place so we need a temporary buffer.
u8 *temp = (u8 *)malloc(decryptedSize);
- _assert_msg_(temp != nullptr, "Failed to allocate gzip decompression buffer (decryptedSize: %d)", decryptedSize);
+ _assert_msg_(temp != nullptr, "Failed to allocate decompression buffer (decryptedSize: %d)", decryptedSize);
memcpy(temp, ptr, decryptedSize);
- int outBytes = gzipDecompress((u8 *)ptr, maxElfSize, temp);
+
+ bool isKL3E = false;
+ int outBytes;
+ const char *scheme;
+ if (IsKL4EMagic(temp, decryptedSize, &isKL3E)) {
+ scheme = isKL3E ? "KL3E" : "KL4E";
+ // The decompressor is handed the stream header, i.e. past the four-byte magic.
+ outBytes = DecompressKL4E((u8 *)ptr, maxElfSize, temp + 4, (size_t)decryptedSize - 4, nullptr, isKL3E);
+ if (outBytes < 0) {
+ // A PSP error code, not a byte count.
+ outBytes = -1;
+ }
+ } else {
+ scheme = "gzip";
+ _dbg_assert_(Read32(ptr + 0x150) != ELF_MAGIC);
+ outBytes = gzipDecompress((u8 *)ptr, maxElfSize, temp);
+ }
free(temp);
if (outBytes < 0) {
- // Not necessarily actually gzip - some kd/ system modules (and possibly VSH
- // modules) use KL4E compression instead, which we don't support decompressing.
- // Bail out cleanly here rather than falling through to parse whatever's left
- // in the buffer (still compressed, not a valid ELF) as if it were real code.
- *error_string = StringFromFormat("Module '%s' decompression failed", head->modname);
+ // Bail out cleanly rather than falling through to parse whatever's left in the
+ // buffer (still compressed, not a valid ELF) as if it were real code.
+ *error_string = StringFromFormat("Module '%s' %s decompression failed", head->modname, scheme);
delete [] newptr;
module->Cleanup();
kernelObjects.Destroy(module->GetUID());
@@ -1334,7 +1349,7 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load
error = SCE_KERNEL_ERROR_FILEERR;
return nullptr;
}
- INFO_LOG(Log::sceModule, "gzip is enabled in '%s', decompressing (%d -> %d bytes, bufmax=%d).", head->modname, decryptedSize, outBytes, maxElfSize);
+ INFO_LOG(Log::sceModule, "'%s' is %s-compressed, decompressing (%d -> %d bytes, bufmax=%d).", head->modname, scheme, decryptedSize, outBytes, maxElfSize);
}
if (fakeLoadedModule) {
diff --git a/Core/Util/KL4E.cpp b/Core/Util/KL4E.cpp
new file mode 100644
index 0000000000..eff3551d82
--- /dev/null
+++ b/Core/Util/KL4E.cpp
@@ -0,0 +1,419 @@
+// Copyright (c) 2026- PPSSPP Project.
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 2.0 or later versions.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License 2.0 for more details.
+
+// A copy of the GPL 2.0 should have been included with the program.
+// If not, see http://www.gnu.org/licenses/
+
+// Official git repository and contact information can be found at
+// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
+
+// KL4E / KL3E decompression. See docs/KL4E.md.
+//
+// LZ77 tokens - "emit this literal" or "repeat N bytes from M back" - where every bit is coded
+// with an adaptive binary arithmetic coder rather than written directly. Structurally close to
+// LZMA. KL3E differs from KL4E in exactly one constant (kPowLimitLong).
+//
+// Written from the format description, not ported from existing code.
+
+#include
+
+#include "Common/Log.h"
+#include "Core/HLE/ErrorCodes.h"
+#include "Core/Util/KL4E.h"
+namespace {
+
+// Adaptation profiles. A probability is the chance the next bit is a 1, out of 256: on a 0 it
+// just decays, on a 1 it decays and then gains the bonus back, so it climbs toward a ceiling of
+// roughly (bonus << decay).
+const int kDecayNormal = 3, kBonusNormal = 31;
+const int kDecayFlag = 4, kBonusFlag = 15;
+
+// Table sizes. Each is exactly large enough for the highest index its indexing scheme can
+// produce - except copyDistProbs, see the bounds check in the distance decode.
+const int kLitProbsSize = 2040; // 8 literal contexts * 255
+const int kCopyCountBitsProbsSize = 64; // 8 states * 8 unary steps
+const int kCopyCountProbsSize = 256;
+const int kCopyDistBitsProbsSize = 304;
+const int kCopyDistProbsSize = 144;
+
+// The doubling walk that sizes a distance code stops once it reaches this. The single constant
+// that separates the two formats.
+const int kPowLimitKL4E = 256;
+const int kPowLimitKL3E = 128;
+// Short matches use a tighter limit in both formats.
+const int kPowLimitShort = 64;
+
+struct ArithDecoder {
+ const u8 *in;
+ const u8 *inEnd;
+ u32 range;
+ u32 code;
+ bool overrun;
+
+ u8 NextByte() {
+ if (in >= inEnd) {
+ // The format carries no length and relies on the stream terminating itself, so a
+ // truncated file would otherwise read forever. Feed zeroes and remember.
+ overrun = true;
+ return 0;
+ }
+ return *in++;
+ }
+
+ // One bit against an adaptive probability.
+ int ReadBit(u8 *prob, int decay, int bonus) {
+ u32 bound;
+ const u32 p = *prob;
+ if ((range >> 24) == 0) {
+ // Renormalize. The bound uses the range from *before* the shift.
+ code = (code << 8) + NextByte();
+ bound = range * p;
+ range <<= 8;
+ } else {
+ bound = (range >> 8) * p;
+ }
+ const u32 decayed = p - (p >> decay);
+ if (code >= bound) {
+ code -= bound;
+ range -= bound;
+ *prob = (u8)decayed;
+ return 0;
+ }
+ range = bound;
+ *prob = (u8)(decayed + bonus);
+ return 1;
+ }
+
+ // A bit the encoder judged incompressible: no probability, no adaptation.
+ int ReadBitUniform() {
+ if ((range >> 24) == 0) {
+ code = (code << 8) + NextByte();
+ range <<= 7;
+ } else {
+ range >>= 1;
+ }
+ if (code >= range) {
+ code -= range;
+ return 0;
+ }
+ return 1;
+ }
+
+ // Same, skipping the refill check. Only valid straight after a read that did refill, which
+ // is how runs of uniform bits are emitted.
+ int ReadBitUniformNoNorm() {
+ range >>= 1;
+ if (code >= range) {
+ code -= range;
+ return 0;
+ }
+ return 1;
+ }
+
+ // Eight bits into an accumulator that starts at 1, so the count is implicit. The context is
+ // a mix of the previous byte and the output position's alignment; `shift` slides the window
+ // between the two, the same choice LZMA's lc makes.
+ u32 ReadLiteral(u8 *litProbs, int outPos, u32 prevByte, int shift) {
+ const u32 ctx = ((u32)(outPos & 7) << 8) | (prevByte & 0xFF);
+ u8 *probs = litProbs + ((ctx >> shift) & 7) * 255 - 1;
+ u32 acc = 1;
+ while (acc < 0x100) {
+ acc = (acc << 1) | ReadBit(&probs[acc], kDecayNormal, kBonusNormal);
+ }
+ return acc & 0xFF;
+ }
+};
+
+} // namespace
+
+bool IsKL4EMagic(const u8 *data, size_t size, bool *isKL3E) {
+ if (size < 4) {
+ return false;
+ }
+ if (!memcmp(data, "KL4E", 4)) {
+ if (isKL3E) {
+ *isKL3E = false;
+ }
+ return true;
+ }
+ if (!memcmp(data, "KL3E", 4)) {
+ if (isKL3E) {
+ *isKL3E = true;
+ }
+ return true;
+ }
+ return false;
+}
+
+int DecompressKL4E(u8 *out, int outSize, const u8 *in, size_t inSize, const u8 **end, bool isKL3E) {
+ if (!out || !in || outSize <= 0 || inSize < 5) {
+ return SCE_KERNEL_ERROR_INVALID_FORMAT;
+ }
+
+ // --- five-byte stream header ---
+ const u8 flags = in[0];
+ // Big-endian, which almost nothing else on the PSP is.
+ const u32 headerWord = ((u32)in[1] << 24) | ((u32)in[2] << 16) | ((u32)in[3] << 8) | in[4];
+
+ if (flags & 0x80) {
+ // Stored, not compressed: the header word is a length and the bytes follow verbatim.
+ // A payload that exactly fills the output buffer is rejected, not accepted.
+ if (headerWord >= (u32)outSize) {
+ return SCE_KERNEL_ERROR_INVALID_SIZE;
+ }
+ if (5 + (size_t)headerWord > inSize) {
+ return SCE_KERNEL_ERROR_INVALID_FORMAT;
+ }
+ memcpy(out, in + 5, headerWord);
+ if (end) {
+ *end = in + 5 + headerWord;
+ }
+ return (int)headerWord;
+ }
+
+ // bits 4-3 pick the starting probability, bits 2-0 the literal context selector.
+ const u8 seed = (u8)(0x80 - (((flags >> 3) & 3) << 4));
+ const int shift = flags & 7;
+
+ u8 litProbs[kLitProbsSize];
+ u8 copyCountBitsProbs[kCopyCountBitsProbsSize];
+ u8 copyCountProbs[kCopyCountProbsSize];
+ u8 copyDistBitsProbs[kCopyDistBitsProbsSize];
+ u8 copyDistProbs[kCopyDistProbsSize];
+ memset(litProbs, seed, sizeof(litProbs));
+ memset(copyCountBitsProbs, seed, sizeof(copyCountBitsProbs));
+ memset(copyCountProbs, seed, sizeof(copyCountProbs));
+ memset(copyDistBitsProbs, seed, sizeof(copyDistBitsProbs));
+ memset(copyDistProbs, seed, sizeof(copyDistProbs));
+
+ ArithDecoder dec;
+ dec.in = in + 5;
+ dec.inEnd = in + inSize;
+ dec.range = 0xFFFFFFFF;
+ dec.code = headerWord;
+ dec.overrun = false;
+
+ const int powLimitLong = isKL3E ? kPowLimitKL3E : kPowLimitKL4E;
+
+ // outPos indexes the byte being produced. The loop head advances it, which is why a match
+ // only advances it by copyCount after writing copyCount + 1 bytes.
+ int outPos = 0;
+ u32 prevByte = 0;
+
+ // An index into copyCountBitsProbs that persists across tokens - the "state". The unary walk
+ // below moves it in steps of 8 and does not put it back, so its low three bits are what the
+ // length code's context uses.
+ int countBitsIdx = 0;
+
+ // The first literal has no flag bit in front of it.
+ prevByte = dec.ReadLiteral(litProbs, outPos, prevByte, shift);
+ out[0] = (u8)prevByte;
+
+ while (true) {
+ outPos++;
+
+ if (dec.overrun) {
+ return SCE_KERNEL_ERROR_INVALID_FORMAT;
+ }
+
+ if (dec.ReadBit(©CountBitsProbs[countBitsIdx], kDecayFlag, kBonusFlag) == 0) {
+ // Literal.
+ countBitsIdx = countBitsIdx > 0 ? countBitsIdx - 1 : 0;
+ if (outPos >= outSize) {
+ return SCE_KERNEL_ERROR_INVALID_SIZE;
+ }
+ prevByte = dec.ReadLiteral(litProbs, outPos, prevByte, shift);
+ out[outPos] = (u8)prevByte;
+ continue;
+ }
+
+ // A match. First, how many bits the length code uses, as a unary run stepping the index
+ // by 8 so the low three bits stay put.
+ u32 copyCount = 1;
+ int copyCountBits = -1;
+ while (copyCountBits < 6) {
+ countBitsIdx += 8;
+ if (countBitsIdx >= kCopyCountBitsProbsSize) {
+ return SCE_KERNEL_ERROR_INVALID_FORMAT;
+ }
+ if (!dec.ReadBit(©CountBitsProbs[countBitsIdx], kDecayFlag, kBonusFlag)) {
+ break;
+ }
+ copyCountBits++;
+ }
+
+ // The length, and with it the distance code's parameters: the bit that lands in the
+ // length's low position also selects them, for short matches.
+ int powLimit = kPowLimitShort;
+ int distBase = copyCountBits;
+ if (copyCountBits >= 0) {
+ const int offset = (copyCountBits << 5)
+ | (((outPos & 3) << (copyCountBits + 3)) & 0x18)
+ | (countBitsIdx & 7);
+ u8 *probs = copyCountProbs + offset;
+
+ // High bits: two adaptive, then uniform for anything longer.
+ if (copyCountBits >= 3) {
+ copyCount = 2 + dec.ReadBit(probs + 24, kDecayNormal, kBonusNormal);
+ if (copyCountBits > 3) {
+ copyCount = (copyCount << 1) | dec.ReadBit(probs + 24, kDecayNormal, kBonusNormal);
+ if (copyCountBits > 4) {
+ copyCount = (copyCount << 1) | dec.ReadBitUniform();
+ }
+ for (int i = 5; i < copyCountBits; i++) {
+ copyCount = (copyCount << 1) | dec.ReadBitUniformNoNorm();
+ }
+ }
+ }
+
+ copyCount <<= 1;
+ if (dec.ReadBit(probs, kDecayNormal, kBonusNormal)) {
+ copyCount |= 1;
+ if (copyCountBits <= 0) {
+ powLimit = powLimitLong;
+ distBase = 56 + copyCountBits;
+ }
+ } else if (copyCountBits <= 0) {
+ powLimit = kPowLimitShort;
+ distBase = copyCountBits;
+ }
+
+ if (copyCountBits > 0) {
+ copyCount = (copyCount << 1) | dec.ReadBit(probs + 8, kDecayNormal, kBonusNormal);
+ if (copyCountBits != 1) {
+ copyCount <<= 1;
+ if (dec.ReadBit(probs + 16, kDecayNormal, kBonusNormal)) {
+ copyCount++;
+ // 0xFF ends the stream. There is no length in the header and no
+ // terminator byte; this is the only normal way out of the loop.
+ if (copyCount == 0xFF) {
+ if (end) {
+ *end = dec.in;
+ }
+ return outPos;
+ }
+ }
+ }
+ powLimit = powLimitLong;
+ distBase = 56 + copyCountBits;
+ }
+ }
+
+ // How many bits the distance code uses, by a doubling walk. curPow only picks up the
+ // extra 8 when the walk continues, which is what makes the deepest reachable index fit
+ // copyDistBitsProbs exactly.
+ u32 copyDist = 0;
+ bool distanceIsZero = false;
+ int curPow = 8;
+ int copyDistBits = 0;
+ while (true) {
+ const int probIndex = distBase + curPow - 7;
+ if (probIndex < 0 || probIndex >= kCopyDistBitsProbsSize) {
+ return SCE_KERNEL_ERROR_INVALID_FORMAT;
+ }
+ u8 *prob = ©DistBitsProbs[probIndex];
+ curPow <<= 1;
+ copyDistBits = curPow - powLimit;
+ if (!dec.ReadBit(prob, kDecayNormal, kBonusNormal)) {
+ if (copyDistBits >= 0) {
+ if (copyDistBits != 0) {
+ copyDistBits -= 8;
+ break;
+ }
+ // No distance bits at all: a match against the byte just emitted, i.e. a run.
+ distanceIsZero = true;
+ break;
+ }
+ } else {
+ curPow += 8;
+ if (copyDistBits >= 0) {
+ break;
+ }
+ }
+ }
+
+ if (!distanceIsZero) {
+ // The distance value. Same shape as the length: two adaptive high bits, uniform in
+ // the middle, then three adaptive low bits whose +1/-1 adjustments keep the code
+ // ranges contiguous across bit counts.
+ if (copyDistBits < 0 || copyDistBits + 3 >= kCopyDistProbsSize) {
+ // Only reachable from a stream encoding a distance far larger than any real
+ // module needs - the hardware routine reads past its own table here.
+ return SCE_KERNEL_ERROR_INVALID_FORMAT;
+ }
+ u8 *probs = copyDistProbs + copyDistBits;
+ int readBits = copyDistBits / 8;
+
+ if (readBits < 3) {
+ copyDist = 1;
+ } else {
+ copyDist = 2 + dec.ReadBit(probs + 3, kDecayNormal, kBonusNormal);
+ if (readBits > 3) {
+ copyDist = (copyDist << 1) | dec.ReadBit(probs + 3, kDecayNormal, kBonusNormal);
+ if (readBits > 4) {
+ copyDist = (copyDist << 1) | dec.ReadBitUniform();
+ readBits--;
+ }
+ while (readBits > 4) {
+ copyDist = (copyDist << 1) + dec.ReadBitUniformNoNorm();
+ readBits--;
+ }
+ }
+ }
+
+ copyDist <<= 1;
+ if (dec.ReadBit(probs, kDecayNormal, kBonusNormal)) {
+ if (readBits > 0) {
+ copyDist++;
+ }
+ } else if (readBits <= 0) {
+ copyDist--;
+ }
+
+ if (readBits > 0) {
+ copyDist <<= 1;
+ if (dec.ReadBit(probs + 1, kDecayNormal, kBonusNormal)) {
+ if (readBits != 1) {
+ copyDist++;
+ }
+ } else if (readBits == 1) {
+ copyDist--;
+ }
+ if (readBits != 1) {
+ copyDist <<= 1;
+ if (!dec.ReadBit(probs + 2, kDecayNormal, kBonusNormal)) {
+ copyDist--;
+ }
+ }
+ }
+
+ if (copyDist >= (u32)outPos) {
+ return SCE_KERNEL_ERROR_INVALID_FORMAT;
+ }
+ }
+
+ const int copyLen = (int)copyCount + 1;
+ // The hardware routine bounds-checks only the literal path, so a crafted stream can
+ // write up to 255 bytes past the end of the output buffer here. Check it.
+ if (copyLen > outSize - outPos) {
+ return SCE_KERNEL_ERROR_INVALID_SIZE;
+ }
+
+ // Forward, byte at a time - overlap is legal and is how runs are encoded.
+ const u8 *src = out + outPos - copyDist - 1;
+ for (int i = 0; i < copyLen; i++) {
+ out[outPos + i] = src[i];
+ }
+ prevByte = out[outPos + (int)copyCount];
+ outPos += (int)copyCount;
+ countBitsIdx = 6 + (outPos & 1);
+ }
+}
diff --git a/Core/Util/KL4E.h b/Core/Util/KL4E.h
new file mode 100644
index 0000000000..e2fbb7345a
--- /dev/null
+++ b/Core/Util/KL4E.h
@@ -0,0 +1,41 @@
+// Copyright (c) 2026- PPSSPP Project.
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 2.0 or later versions.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License 2.0 for more details.
+
+// A copy of the GPL 2.0 should have been included with the program.
+// If not, see http://www.gnu.org/licenses/
+
+// Official git repository and contact information can be found at
+// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
+
+#pragma once
+
+#include
+#include "Common/CommonTypes.h"
+
+// KL4E / KL3E - Sony's LZ77-with-arithmetic-coding scheme for compressed PSP executables, the
+// alternative to gzip inside a ~PSP module. KL3E is the same bitstream with one constant changed.
+//
+// See docs/KL4E.md for the format.
+
+// Decompresses one KL4E/KL3E stream. `in` points at the five-byte stream header, i.e. *past* the
+// four-byte "KL4E"/"KL3E" magic. `inSize` bounds reads so a truncated file can't run off the end.
+//
+// Returns the number of bytes written, or a negative PSP error code:
+// SCE_KERNEL_ERROR_INVALID_SIZE (0x80000104) output didn't fit
+// SCE_KERNEL_ERROR_INVALID_FORMAT (0x80000108) stream is malformed
+//
+// If `end` is non-null it receives a pointer just past the last input byte consumed. (The PSP's
+// own routine reports the last byte consumed for stored streams and one past for compressed ones;
+// this is deliberately consistent instead, since nothing in PPSSPP depends on the quirk.)
+int DecompressKL4E(u8 *out, int outSize, const u8 *in, size_t inSize, const u8 **end, bool isKL3E);
+
+// True if the buffer starts with a "KL4E" or "KL3E" magic. Sets *isKL3E when it does.
+bool IsKL4EMagic(const u8 *data, size_t size, bool *isKL3E);
diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj
index e3a4523a23..d043148711 100644
--- a/UWP/CoreUWP/CoreUWP.vcxproj
+++ b/UWP/CoreUWP/CoreUWP.vcxproj
@@ -336,6 +336,7 @@
+
@@ -655,6 +656,7 @@
+
diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters
index bb2f5192e8..20fee321d7 100644
--- a/UWP/CoreUWP/CoreUWP.vcxproj.filters
+++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters
@@ -279,6 +279,7 @@
+
diff --git a/android/jni/Android.mk b/android/jni/Android.mk
index eae8b2235a..d62a3e1e35 100644
--- a/android/jni/Android.mk
+++ b/android/jni/Android.mk
@@ -811,6 +811,7 @@ EXEC_AND_LIB_FILES := \
$(SRC)/Core/Util/GameManager.cpp \
$(SRC)/Core/Util/BlockAllocator.cpp \
$(SRC)/Core/Util/PPGeDraw.cpp \
+ $(SRC)/Core/Util/KL4E.cpp \
$(SRC)/Core/Util/PSARUnpack.cpp \
$(SRC)/Core/Util/RecentFiles.cpp \
$(SRC)/Core/Util/VideoPlayer.cpp \
diff --git a/libretro/Makefile.common b/libretro/Makefile.common
index a0f8f7fd65..b197b43ffe 100644
--- a/libretro/Makefile.common
+++ b/libretro/Makefile.common
@@ -900,6 +900,7 @@ SOURCES_CXX += \
$(COREDIR)/Util/BlockAllocator.cpp \
$(COREDIR)/Util/MemStick.cpp \
$(COREDIR)/Util/PPGeDraw.cpp \
+ $(COREDIR)/Util/KL4E.cpp \
$(COREDIR)/Util/PSARUnpack.cpp \
$(COREDIR)/Util/RecentFiles.cpp \
$(COREDIR)/Util/AudioFormat.cpp \