diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt
index 2babc86f..15d65623 100644
--- a/app/CMakeLists.txt
+++ b/app/CMakeLists.txt
@@ -43,6 +43,7 @@ add_library(skyline SHARED
         ${source_DIR}/skyline/loader/loader.cpp
         ${source_DIR}/skyline/loader/nro.cpp
         ${source_DIR}/skyline/loader/nso.cpp
+        ${source_DIR}/skyline/loader/nca.cpp
         ${source_DIR}/skyline/kernel/memory.cpp
         ${source_DIR}/skyline/kernel/ipc.cpp
         ${source_DIR}/skyline/kernel/svc.cpp
@@ -104,6 +105,7 @@ add_library(skyline SHARED
         ${source_DIR}/skyline/vfs/rom_filesystem.cpp
         ${source_DIR}/skyline/vfs/os_backing.cpp
         ${source_DIR}/skyline/vfs/nacp.cpp
+        ${source_DIR}/skyline/vfs/nca.cpp
         )
 
 target_link_libraries(skyline vulkan android fmt tinyxml2 oboe lz4_static)
diff --git a/app/src/main/cpp/loader_jni.cpp b/app/src/main/cpp/loader_jni.cpp
index aa83cff0..c1ce0f6a 100644
--- a/app/src/main/cpp/loader_jni.cpp
+++ b/app/src/main/cpp/loader_jni.cpp
@@ -4,6 +4,7 @@
 #include "skyline/vfs/os_backing.h"
 #include "skyline/loader/nro.h"
 #include "skyline/loader/nso.h"
+#include "skyline/loader/nca.h"
 #include "skyline/jvm.h"
 
 extern "C" JNIEXPORT jlong JNICALL Java_emu_skyline_loader_RomFile_initialize(JNIEnv *env, jobject thiz, jint jformat, jint fd) {
@@ -17,6 +18,8 @@ extern "C" JNIEXPORT jlong JNICALL Java_emu_skyline_loader_RomFile_initialize(JN
                 return reinterpret_cast<jlong>(new skyline::loader::NroLoader(backing));
             case skyline::loader::RomFormat::NSO:
                 return reinterpret_cast<jlong>(new skyline::loader::NsoLoader(backing));
+            case skyline::loader::RomFormat::NCA:
+                return reinterpret_cast<jlong>(new skyline::loader::NcaLoader(backing));
             default:
                 return 0;
         }
@@ -52,4 +55,4 @@ extern "C" JNIEXPORT jstring JNICALL Java_emu_skyline_loader_RomFile_getApplicat
 
 extern "C" JNIEXPORT void JNICALL Java_emu_skyline_loader_RomFile_destroy(JNIEnv *env, jobject thiz, jlong instance) {
     delete reinterpret_cast<skyline::loader::NroLoader *>(instance);
-}
\ No newline at end of file
+}
diff --git a/app/src/main/cpp/skyline/kernel/memory.h b/app/src/main/cpp/skyline/kernel/memory.h
index 3fb091cd..1d90955c 100644
--- a/app/src/main/cpp/skyline/kernel/memory.h
+++ b/app/src/main/cpp/skyline/kernel/memory.h
@@ -213,6 +213,7 @@ namespace skyline {
     namespace loader {
         class NroLoader;
         class NsoLoader;
+        class NcaLoader;
     }
 
     namespace kernel {
@@ -324,6 +325,7 @@ namespace skyline {
             friend class type::KProcess;
             friend class loader::NroLoader;
             friend class loader::NsoLoader;
+            friend class loader::NcaLoader;
 
             friend void svc::SetMemoryAttribute(DeviceState &state);
 
diff --git a/app/src/main/cpp/skyline/loader/loader.h b/app/src/main/cpp/skyline/loader/loader.h
index 1fe4baf1..9ed7df7b 100644
--- a/app/src/main/cpp/skyline/loader/loader.h
+++ b/app/src/main/cpp/skyline/loader/loader.h
@@ -15,6 +15,7 @@ namespace skyline::loader {
     enum class RomFormat {
         NRO, //!< The NRO format: https://switchbrew.org/wiki/NRO
         NSO, //!< The NSO format: https://switchbrew.org/wiki/NSO
+        NCA, //!< The NCA format: https://switchbrew.org/wiki/NCA
         XCI, //!< The XCI format: https://switchbrew.org/wiki/XCI
         NSP, //!< The NSP format from "nspwn" exploit: https://switchbrew.org/wiki/Switch_System_Flaws
     };
diff --git a/app/src/main/cpp/skyline/loader/nca.cpp b/app/src/main/cpp/skyline/loader/nca.cpp
new file mode 100644
index 00000000..d61d11b3
--- /dev/null
+++ b/app/src/main/cpp/skyline/loader/nca.cpp
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: MPL-2.0
+// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
+
+#include <nce.h>
+#include <os.h>
+#include <kernel/memory.h>
+#include "nso.h"
+#include "nca.h"
+
+namespace skyline::loader {
+    NcaLoader::NcaLoader(const std::shared_ptr<vfs::Backing> &backing) : nca(backing) {
+        if (nca.exeFs == nullptr)
+            throw exception("Only NCAs with an ExeFS can be loaded directly");
+    }
+
+    void NcaLoader::LoadExeFs(const std::shared_ptr<vfs::FileSystem> &exeFs, const std::shared_ptr<kernel::type::KProcess> process, const DeviceState &state) {
+        if (exeFs == nullptr)
+            throw exception("Cannot load a null ExeFS");
+
+        auto nsoFile = exeFs->OpenFile("rtld");
+        if (nsoFile == nullptr)
+            throw exception("Cannot load an ExeFS that doesn't contain rtld");
+
+        auto loadInfo = NsoLoader::LoadNso(nsoFile, process, state);
+        u64 offset = loadInfo.size;
+        u64 base = loadInfo.base;
+
+        state.logger->Info("Loaded nso 'rtld' at 0x{:X}", base);
+
+        for (const auto &nso : {"main", "subsdk0", "subsdk1", "subsdk2", "subsdk3", "subsdk4", "subsdk5", "subsdk6", "subsdk7", "sdk"}) {
+            nsoFile = exeFs->OpenFile(nso);
+
+            if (nsoFile == nullptr)
+                continue;
+
+            loadInfo = NsoLoader::LoadNso(nsoFile, process, state, offset);
+            state.logger->Info("Loaded nso '{}' at 0x{:X}", nso, base + offset);
+            offset += loadInfo.size;
+        }
+
+        state.os->memory.InitializeRegions(base, offset, memory::AddressSpaceType::AddressSpace39Bit);
+    }
+
+    void NcaLoader::LoadProcessData(const std::shared_ptr<kernel::type::KProcess> process, const DeviceState &state) {
+        LoadExeFs(nca.exeFs, process, state);
+    }
+}
diff --git a/app/src/main/cpp/skyline/loader/nca.h b/app/src/main/cpp/skyline/loader/nca.h
new file mode 100644
index 00000000..71bcd90c
--- /dev/null
+++ b/app/src/main/cpp/skyline/loader/nca.h
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: MPL-2.0
+// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
+
+#pragma once
+
+#include <common.h>
+#include <vfs/nca.h>
+#include "loader.h"
+
+namespace skyline::loader {
+    /**
+     * @brief The NcaLoader class allows loading an NCA's ExeFS through the Loader interface (https://switchbrew.org/wiki/NSO)
+     */
+    class NcaLoader : public Loader {
+      private:
+        vfs::NCA nca; //!< The backing NCA of the loader
+
+      public:
+        NcaLoader(const std::shared_ptr<vfs::Backing> &backing);
+
+        /**
+         * @brief This loads an ExeFS into memory
+         * @param exefs A filesystem object containing the ExeFS filesystem to load into memory
+         * @param process The process to load the ExeFS into
+         */
+        static void LoadExeFs(const std::shared_ptr<vfs::FileSystem> &exefs, const std::shared_ptr<kernel::type::KProcess> process, const DeviceState &state);
+
+        void LoadProcessData(const std::shared_ptr<kernel::type::KProcess> process, const DeviceState &state);
+    };
+}
diff --git a/app/src/main/cpp/skyline/os.cpp b/app/src/main/cpp/skyline/os.cpp
index d6916a5c..6cbb662a 100644
--- a/app/src/main/cpp/skyline/os.cpp
+++ b/app/src/main/cpp/skyline/os.cpp
@@ -4,6 +4,7 @@
 #include "vfs/os_backing.h"
 #include "loader/nro.h"
 #include "loader/nso.h"
+#include "loader/nca.h"
 #include "nce/guest.h"
 #include "os.h"
 
@@ -17,6 +18,8 @@ namespace skyline::kernel {
             state.loader = std::make_shared<loader::NroLoader>(romFile);
         } else if (romType == loader::RomFormat::NSO) {
             state.loader = std::make_shared<loader::NsoLoader>(romFile);
+        } else if (romType == loader::RomFormat::NCA) {
+            state.loader = std::make_shared<loader::NcaLoader>(romFile);
         } else {
             throw exception("Unsupported ROM extension.");
         }
diff --git a/app/src/main/cpp/skyline/vfs/nca.cpp b/app/src/main/cpp/skyline/vfs/nca.cpp
new file mode 100644
index 00000000..fec41842
--- /dev/null
+++ b/app/src/main/cpp/skyline/vfs/nca.cpp
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: MPL-2.0
+// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
+
+#include "region_backing.h"
+#include "partition_filesystem.h"
+#include "nca.h"
+#include "rom_filesystem.h"
+#include "directory.h"
+
+namespace skyline::vfs {
+    NCA::NCA(const std::shared_ptr<vfs::Backing> &backing) : backing(backing) {
+        backing->Read(&header);
+
+        if (header.magic != util::MakeMagic<u32>("NCA3"))
+            throw exception("Attempting to load an encrypted or invalid NCA");
+
+        contentType = header.contentType;
+
+        for (size_t i = 0; i < header.sectionHeaders.size(); i++) {
+            auto &sectionHeader = header.sectionHeaders.at(i);
+            auto &sectionEntry = header.fsEntries.at(i);
+
+            if (sectionHeader.fsType == NcaSectionFsType::PFS0 && sectionHeader.hashType == NcaSectionHashType::HierarchicalSha256)
+                ReadPfs0(sectionHeader, sectionEntry);
+            else if (sectionHeader.fsType == NcaSectionFsType::RomFs && sectionHeader.hashType == NcaSectionHashType::HierarchicalIntegrity)
+                ReadRomFs(sectionHeader, sectionEntry);
+        }
+    }
+
+    void NCA::ReadPfs0(const NcaSectionHeader &header, const NcaFsEntry &entry) {
+        size_t offset = static_cast<size_t>(entry.startOffset) * constant::MediaUnitSize + header.sha256HashInfo.pfs0Offset;
+        size_t size = constant::MediaUnitSize * static_cast<size_t>(entry.endOffset - entry.startOffset);
+
+        auto pfs = std::make_shared<PartitionFileSystem>(std::make_shared<RegionBacking>(backing, offset, size));
+
+        if (contentType == NcaContentType::Program) {
+            // An ExeFS must always contain an NPDM and a main NSO, whereas the logo section will always contain a logo and a startup movie
+            if (pfs->FileExists("main") && pfs->FileExists("main.npdm"))
+                exeFs = std::move(pfs);
+            else if (pfs->FileExists("NintendoLogo.png") && pfs->FileExists("StartupMovie.gif"))
+                logo = std::move(pfs);
+        } else if (contentType == NcaContentType::Meta) {
+            cnmt = std::move(pfs);
+        }
+    }
+
+    void NCA::ReadRomFs(const NcaSectionHeader &header, const NcaFsEntry &entry) {
+        size_t offset = static_cast<size_t>(entry.startOffset) * constant::MediaUnitSize + header.integrityHashInfo.levels.back().offset;
+        size_t size = header.integrityHashInfo.levels.back().size;
+
+        romFs = std::make_shared<RegionBacking>(backing, offset, size);
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/cpp/skyline/vfs/nca.h b/app/src/main/cpp/skyline/vfs/nca.h
new file mode 100644
index 00000000..4e12b74d
--- /dev/null
+++ b/app/src/main/cpp/skyline/vfs/nca.h
@@ -0,0 +1,213 @@
+// SPDX-License-Identifier: MPL-2.0
+// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
+
+#pragma once
+
+#include <array>
+#include "filesystem.h"
+
+namespace skyline {
+    namespace constant {
+        constexpr size_t MediaUnitSize = 0x200; //!< The unit size of entries in an NCA
+    }
+
+    namespace vfs {
+        /**
+         * @brief This enumerates the various content types of an NCA
+         */
+        enum class NcaContentType : u8 {
+            Program = 0x0, //!< This is a program NCA
+            Meta = 0x1, //!< This is a metadata NCA
+            Control = 0x2, //!< This is a control NCA
+            Manual = 0x3, //!< This is a manual NCA
+            Data = 0x4, //!< This is a data NCA
+            PublicData = 0x5, //!< This is a public data NCA
+        };
+
+        /**
+         * @brief The NCA class provides an easy way to access the contents of an NCA file (https://switchbrew.org/wiki/NCA_Format)
+         */
+        class NCA {
+          private:
+            /**
+             * @brief This enumerates the distribution types of an NCA
+             */
+            enum class NcaDistributionType : u8 {
+                System = 0x0, //!< This NCA was distributed on the EShop or is part of the system
+                GameCard = 0x1, //!< This NCA was distributed on a GameCard
+            };
+
+            /**
+             * @brief This enumerates the key generation version in NCAs before HOS 3.0.1
+             */
+            enum class NcaLegacyKeyGenerationType : u8 {
+                Fw100 = 0x0, //!< 1.0.0
+                Fw300 = 0x2, //!< 3.0.0
+            };
+
+            /**
+             * @brief This enumerates the key generation version in NCAs after HOS 3.0.0
+             */
+            enum class NcaKeyGenerationType : u8 {
+                Fw301 = 0x3, //!< 3.0.1
+                Fw400 = 0x4, //!< 4.0.0
+                Fw500 = 0x5, //!< 5.0.0
+                Fw600 = 0x6, //!< 6.0.0
+                Fw620 = 0x7, //!< 6.2.0
+                Fw700 = 0x8, //!< 7.0.0
+                Fw810 = 0x9, //!< 8.1.0
+                Fw900 = 0xa, //!< 9.0.0
+                Fw910 = 0xb, //!< 9.1.0
+                Invalid = 0xff, //!< An invalid key generation type
+            };
+
+            /**
+             * @brief This enumerates the key area encryption key types
+             */
+            enum class NcaKeyAreaEncryptionKeyType : u8 {
+                Application = 0x0, //!< This NCA uses the application key encryption area
+                Ocean = 0x1, //!< This NCA uses the ocean key encryption area
+                System = 0x2, //!< This NCA uses the system key encryption area
+            };
+
+            /**
+             * @brief This hold the entry of a single filesystem in an NCA
+             */
+            struct NcaFsEntry {
+                u32 startOffset; //!< The start offset of the filesystem in units of 0x200 bytes
+                u32 endOffset; //!< The start offset of the filesystem in units of 0x200 bytes
+                u64 _pad_;
+            };
+
+            /**
+             * @brief This enumerates the possible filesystem types a section of an NCA can contain
+             */
+            enum class NcaSectionFsType : u8 {
+                RomFs = 0x0, //!< This section contains a RomFs filesystem
+                PFS0 = 0x1, //!< This section contains a PFS0 filesystem
+            };
+
+            /**
+             * @brief This enumerates the possible hash header types of an NCA section
+             */
+            enum class NcaSectionHashType : u8 {
+                HierarchicalSha256 = 0x2, //!< The hash header for this section is that of a PFS0
+                HierarchicalIntegrity = 0x3, //!< The hash header for this section is that of a RomFS
+            };
+
+            /**
+             * @brief This enumerates the possible encryption types of an NCA section
+             */
+            enum class NcaSectionEncryptionType : u8 {
+                None = 0x1, //!< This NCA doesn't use any encryption
+                XTS = 0x2, //!< This NCA uses AES-XTS encryption
+                CTR = 0x3, //!< This NCA uses AES-CTR encryption
+                BKTR = 0x4, //!< This NCA uses BKTR together AES-CTR encryption
+            };
+
+            /**
+             * @brief This holds the data for a single level of the hierarchical integrity scheme
+             */
+            struct HierarchicalIntegrityLevel {
+                u64 offset; //!< The offset of the level data
+                u64 size; //!< The size of the level data
+                u32 blockSize; //!< The block size of the level data
+                u32 _pad_;
+            };
+            static_assert(sizeof(HierarchicalIntegrityLevel) == 0x18);
+
+            /**
+             * @brief This holds the hash info header of the hierarchical integrity scheme
+             */
+            struct HierarchicalIntegrityHashInfo {
+                u32 magic; //!< The hierarchical integrity magic, 'IVFC'
+                u32 magicNumber; //!< The magic number 0x2000
+                u32 masterHashSize; //!< The size of the master hash
+                u32 numLevels; //!< The number of levels
+                std::array<HierarchicalIntegrityLevel, 6> levels; //!< An array of the hierarchical integrity levels
+                u8 _pad0_[0x20];
+                std::array<u8, 0x20> masterHash; //!< The master hash of the hierarchical integrity system
+                u8 _pad1_[0x18];
+            };
+            static_assert(sizeof(HierarchicalIntegrityHashInfo) == 0xf8);
+
+            /**
+             * @brief This holds the hash info header of the SHA256 hashing scheme for PFS0
+             */
+            struct HierarchicalSha256HashInfo {
+                std::array<u8, 0x20> hashTableHash; //!< A SHA256 hash over the hash table
+                u32 blockSize; //!< The block size of the filesystem
+                u32 _pad_;
+                u64 hashTableOffset; //!< The offset from the end of the section header of the hash table
+                u64 hashTableSize; //!< The size of the hash table
+                u64 pfs0Offset; //!< The offset from the end of the section header of the PFS0
+                u64 pfs0Size; //!< The size of the PFS0
+                u8 _pad1_[0xb0];
+            };
+            static_assert(sizeof(HierarchicalSha256HashInfo) == 0xf8);
+
+            /**
+             * @brief This holds the header of each specific section in an NCA
+             */
+            struct NcaSectionHeader {
+                u16 version; //!< The version, always 2
+                NcaSectionFsType fsType; //!< The type of the filesystem in the section
+                NcaSectionHashType hashType; //!< The type of hash header that is used for this section
+                NcaSectionEncryptionType encryptionType; //!< The type of encryption that is used for this section
+                u8 _pad0_[0x3];
+                union {
+                    HierarchicalIntegrityHashInfo integrityHashInfo; //!< The HashInfo used for RomFS
+                    HierarchicalSha256HashInfo sha256HashInfo; //!< The HashInfo used for PFS0
+                };
+                u8 _pad1_[0x40]; // PatchInfo
+                u32 generation; //!< The generation of the NCA section
+                u32 secureValue; //!< The secure value of the section
+                u8 _pad2_[0x30]; //!< SparseInfo
+                u8 _pad3_[0x88];
+            };
+            static_assert(sizeof(NcaSectionHeader) == 0x200);
+
+            /**
+             * @brief This struct holds the header of a Nintendo Content Archive
+             */
+            struct NcaHeader {
+                std::array<u8, 0x100> fixed_key_sig; //!< An RSA-PSS signature over the header with fixed key
+                std::array<u8, 0x100> npdm_key_sig; //!< An RSA-PSS signature over header with key in NPDM
+                u32 magic; //!< The magic of the NCA: 'NCA3'
+                NcaDistributionType distributionType; //!< Whether this NCA is from a gamecard or the E-Shop
+                NcaContentType contentType; //!< The content type of the NCA
+                NcaLegacyKeyGenerationType legacyKeyGenerationType; //!< The keyblob to use for decryption
+                NcaKeyAreaEncryptionKeyType keyAreaEncryptionKeyType; //!< The index of the key area encryption key that is needed
+                u64 size; //!< The total size of the NCA
+                u64 programId; //!< The program ID of the NCA
+                u32 contentIndex; //!< The index of the content
+                u32 sdkVersion; //!< The version of the SDK the NCA was built with
+                NcaKeyGenerationType keyGenerationType; //!< The keyblob to use for decryption
+                u8 fixedKeyGeneration; //!< The fixed key index
+                u8 _pad0_[0xe];
+                std::array<u8, 0x10> rightsId; //!< The NCA's rights ID
+                std::array<NcaFsEntry, 4> fsEntries; //!< The filesystem entries for this NCA
+                std::array<std::array<u8, 0x20>, 4> sectionHashes; //!< This contains SHA-256 hashes for each filesystem header
+                std::array<std::array<u8, 0x10>, 4> encryptedKeyArea; //!< The encrypted key area for each filesystem
+                u8 _pad1_[0xc0];
+                std::array<NcaSectionHeader, 4> sectionHeaders;
+            } header{};
+            static_assert(sizeof(NcaHeader) == 0xc00);
+
+            std::shared_ptr<Backing> backing; //!< The backing for the NCA
+
+            void ReadPfs0(const NcaSectionHeader &header, const NcaFsEntry &entry);
+
+            void ReadRomFs(const NcaSectionHeader &header, const NcaFsEntry &entry);
+
+          public:
+            std::shared_ptr<FileSystem> exeFs; //!< The PFS0 filesystem for this NCA's ExeFS section
+            std::shared_ptr<FileSystem> logo; //!< The PFS0 filesystem for this NCA's logo section
+            std::shared_ptr<FileSystem> cnmt; //!< The PFS0 filesystem for this NCA's CNMT section
+            std::shared_ptr<Backing> romFs; //!< The backing for this NCA's RomFS section
+            NcaContentType contentType; //!< The content type of the NCA
+
+            NCA(const std::shared_ptr<vfs::Backing> &backing);
+        };
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/emu/skyline/MainActivity.kt b/app/src/main/java/emu/skyline/MainActivity.kt
index 0784da0c..239ce437 100644
--- a/app/src/main/java/emu/skyline/MainActivity.kt
+++ b/app/src/main/java/emu/skyline/MainActivity.kt
@@ -113,6 +113,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick
 
                 var foundRoms = addEntries("nro", RomFormat.NRO, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!)
                 foundRoms = foundRoms or addEntries("nso", RomFormat.NSO, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!)
+                foundRoms = foundRoms or addEntries("nca", RomFormat.NCA, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!)
 
                 runOnUiThread {
                     if (!foundRoms)
diff --git a/app/src/main/java/emu/skyline/loader/RomFile.kt b/app/src/main/java/emu/skyline/loader/RomFile.kt
index 7ad9d920..fd869a55 100644
--- a/app/src/main/java/emu/skyline/loader/RomFile.kt
+++ b/app/src/main/java/emu/skyline/loader/RomFile.kt
@@ -25,8 +25,9 @@ import java.util.*
 enum class RomFormat(val format: Int){
     NRO(0),
     NSO(1),
-    XCI(2),
-    NSP(3),
+    NCA(2),
+    XCI(3),
+    NSP(4),
 }
 
 /**