mirror of
https://git.eden-emu.dev/eden-emu/eden
synced 2026-10-22 01:10:02 -04:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e73fda6620 | |||
| aa1441437e | |||
| 79603bfdc7 | |||
| 0ee9051f59 | |||
| 7003176643 | |||
| 650fc625f1 | |||
| 296db5e1ee | |||
| 35cb16f8ca | |||
| 77a5e306e9 | |||
| 263c048a14 | |||
| 9ada6f386a | |||
| 6f5a89e9b5 |
@@ -100,7 +100,7 @@ class Game(
|
||||
|
||||
companion object {
|
||||
val extensions: Set<String> = HashSet(
|
||||
listOf("xci", "nsp", "nca", "nro")
|
||||
listOf("xci", "nsp", "nca", "nro", "nsz", "xcz")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ object GameHelper {
|
||||
|
||||
// File extensions considered as external content, buuut should
|
||||
// be done better imo.
|
||||
private val externalContentExtensions = setOf("nsp", "xci")
|
||||
private val externalContentExtensions = setOf("nsp", "xci", "nsz", "xcz")
|
||||
|
||||
private fun scanContentContainersRecursive(
|
||||
files: Array<MinimalDocumentFile>,
|
||||
|
||||
@@ -76,7 +76,9 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsValid(JNIEnv* env, jobj
|
||||
auto const file_type = loader->GetFileType();
|
||||
if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error)
|
||||
return false;
|
||||
if ((file_type == Loader::FileType::NSP || file_type == Loader::FileType::XCI) && !Loader::IsBootableGameContainer(file, file_type))
|
||||
if ((file_type == Loader::FileType::NSP || file_type == Loader::FileType::XCI ||
|
||||
file_type == Loader::FileType::NSZ || file_type == Loader::FileType::XCZ) &&
|
||||
!Loader::IsBootableGameContainer(file, file_type))
|
||||
return false;
|
||||
u64 program_id = 0;
|
||||
return loader->ReadProgramId(program_id) == Loader::ResultStatus::Success;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -46,4 +49,10 @@ std::vector<u8> DecompressDataZSTD(std::span<const u8> compressed) {
|
||||
return decompressed;
|
||||
}
|
||||
|
||||
std::size_t DecompressDataZSTD(std::span<const u8> compressed, std::span<u8> output) {
|
||||
const std::size_t result =
|
||||
ZSTD_decompress(output.data(), output.size(), compressed.data(), compressed.size());
|
||||
return ZSTD_isError(result) ? 0 : result;
|
||||
}
|
||||
|
||||
} // namespace Common::Compression
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -42,4 +45,15 @@ namespace Common::Compression {
|
||||
*/
|
||||
[[nodiscard]] std::vector<u8> DecompressDataZSTD(std::span<const u8> compressed);
|
||||
|
||||
/**
|
||||
* Decompresses a single Zstandard frame into a caller-provided buffer of known size.
|
||||
*
|
||||
* @param compressed the compressed source memory region.
|
||||
* @param output the destination buffer. Its size is the maximum number of bytes to write.
|
||||
*
|
||||
* @return the number of bytes written, or 0 if the frame is invalid or doesn't fit. An empty
|
||||
* frame also returns 0.
|
||||
*/
|
||||
[[nodiscard]] std::size_t DecompressDataZSTD(std::span<const u8> compressed, std::span<u8> output);
|
||||
|
||||
} // namespace Common::Compression
|
||||
|
||||
@@ -33,6 +33,8 @@ add_library(core STATIC
|
||||
crypto/encryption_layer.h
|
||||
crypto/key_manager.cpp
|
||||
crypto/key_manager.h
|
||||
crypto/ncz_decompressor.cpp
|
||||
crypto/ncz_decompressor.h
|
||||
crypto/partition_data_manager.cpp
|
||||
crypto/partition_data_manager.h
|
||||
crypto/xts_encryption_layer.cpp
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <zstd.h>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "common/logging.h"
|
||||
#include "common/zstd_compression.h"
|
||||
#include "core/crypto/aes_util.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/crypto/ncz_decompressor.h"
|
||||
#include "core/file_sys/vfs/vfs.h"
|
||||
|
||||
namespace Core::Crypto {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr u64 HeaderSize = 0x4000;
|
||||
constexpr std::size_t SectionWire = 0x40;
|
||||
constexpr std::size_t BlockFixed = 0x18;
|
||||
constexpr std::size_t Chunk = 0x100000;
|
||||
// Upper bound on NCZ section entries (64 bytes each, so the table is at most 4 MiB). A base NCA
|
||||
// has a handful of sections, but nsz stores an update's patch (BKTR) data one entry per
|
||||
// subsection, which is thousands of them.
|
||||
constexpr u64 MaxSectionCount = 0x10000;
|
||||
constexpr std::array<u8, 8> SectionsMagic{'N', 'C', 'Z', 'S', 'E', 'C', 'T', 'N'};
|
||||
constexpr std::array<u8, 8> BlockMagic{'N', 'C', 'Z', 'B', 'L', 'O', 'C', 'K'};
|
||||
|
||||
struct Section {
|
||||
s64 offset, size;
|
||||
u64 crypto_type;
|
||||
Key128 key, counter;
|
||||
bool Ctr() const { return crypto_type == 3 || crypto_type == 4; }
|
||||
};
|
||||
|
||||
struct BlockHeader {
|
||||
u32 exponent;
|
||||
u64 decompressed_size;
|
||||
std::vector<u32> sizes;
|
||||
};
|
||||
|
||||
u64 Le(const u8* p, std::size_t n) {
|
||||
u64 v = 0;
|
||||
for (std::size_t i = n; i-- > 0;) v = (v << 8) | p[i];
|
||||
return v;
|
||||
}
|
||||
|
||||
// Mirrors nsz AESCTR.seek(): upper 8 bytes = stored counter, lower 8 = absolute NCA offset / 16.
|
||||
void SetIv(Key128& iv, const Key128& base, u64 off) {
|
||||
iv = base;
|
||||
u64 blk = off >> 4;
|
||||
for (std::size_t i = 0; i < 8; ++i) { iv[15 - i] = static_cast<u8>(blk & 0xFF); blk >>= 8; }
|
||||
}
|
||||
|
||||
// Parse header + section table + optional block header. Returns payload_start.
|
||||
std::optional<std::pair<std::vector<Section>, std::optional<BlockHeader>>> ParseHeader(
|
||||
const FileSys::VirtualFile& f, u64& payload_start) {
|
||||
const u64 fsize = f->GetSize();
|
||||
if (fsize < HeaderSize + 16) return std::nullopt;
|
||||
|
||||
const auto head = f->ReadBytes(16, HeaderSize);
|
||||
if (head.size() != 16 || !std::equal(head.begin(), head.begin() + 8, SectionsMagic.begin()))
|
||||
return std::nullopt;
|
||||
const u64 count = Le(head.data() + 8, 8);
|
||||
if (count == 0 || count > MaxSectionCount || count * SectionWire > fsize - HeaderSize - 16)
|
||||
return std::nullopt;
|
||||
|
||||
const auto table = f->ReadBytes(count * SectionWire, HeaderSize + 16);
|
||||
if (table.size() != count * SectionWire) return std::nullopt;
|
||||
|
||||
std::vector<Section> sections;
|
||||
sections.reserve(count + 1);
|
||||
for (u64 i = 0; i < count; ++i) {
|
||||
const u8* e = table.data() + i * SectionWire;
|
||||
Section s{};
|
||||
s.offset = static_cast<s64>(Le(e, 8));
|
||||
s.size = static_cast<s64>(Le(e + 8, 8));
|
||||
s.crypto_type = Le(e + 16, 8);
|
||||
std::memcpy(s.key.data(), e + 32, 16);
|
||||
std::memcpy(s.counter.data(), e + 48, 16);
|
||||
sections.push_back(s);
|
||||
}
|
||||
|
||||
const u64 block_start = HeaderSize + 16 + count * SectionWire;
|
||||
const auto fixed = f->ReadBytes(BlockFixed, block_start);
|
||||
std::optional<BlockHeader> bh;
|
||||
if (fixed.size() == BlockFixed &&
|
||||
std::equal(fixed.begin(), fixed.begin() + 8, BlockMagic.begin())) {
|
||||
BlockHeader b{};
|
||||
b.exponent = fixed[11];
|
||||
const s32 n = static_cast<s32>(Le(fixed.data() + 12, 4));
|
||||
if (n <= 0) return std::nullopt;
|
||||
b.decompressed_size = Le(fixed.data() + 16, 8);
|
||||
const auto sizes = f->ReadBytes(static_cast<u64>(n) * 4, block_start + BlockFixed);
|
||||
if (sizes.size() != static_cast<u64>(n) * 4) return std::nullopt;
|
||||
b.sizes.resize(n);
|
||||
for (s32 i = 0; i < n; ++i) b.sizes[i] = static_cast<u32>(Le(sizes.data() + i * 4, 4));
|
||||
payload_start = block_start + BlockFixed + static_cast<u64>(n) * 4;
|
||||
bh = std::move(b);
|
||||
} else {
|
||||
payload_start = block_start;
|
||||
}
|
||||
|
||||
if (!sections.empty() && sections[0].offset > static_cast<s64>(HeaderSize)) {
|
||||
Section fake{};
|
||||
fake.offset = static_cast<s64>(HeaderSize);
|
||||
fake.size = sections[0].offset - static_cast<s64>(HeaderSize);
|
||||
sections.insert(sections.begin(), fake);
|
||||
}
|
||||
return std::make_pair(std::move(sections), std::move(bh));
|
||||
}
|
||||
|
||||
// Sequential reader over independently-compressed blocks, one block cached (like nsz_rs).
|
||||
class BlockReader {
|
||||
public:
|
||||
BlockReader(FileSys::VirtualFile f, const BlockHeader& bh, u64 payload_start)
|
||||
: source{std::move(f)}, block_size{1ULL << bh.exponent}, sizes{bh.sizes},
|
||||
decompressed_size{bh.decompressed_size} {
|
||||
if (bh.exponent < 14 || bh.exponent > 32 || sizes.empty())
|
||||
throw std::runtime_error("bad NCZBLOCK header");
|
||||
u64 off = payload_start;
|
||||
offsets.reserve(sizes.size());
|
||||
for (u32 s : sizes) { offsets.push_back(off); off += s; }
|
||||
}
|
||||
|
||||
// Fills `out` from `position`; returns bytes written.
|
||||
std::size_t ReadAt(u64 position, std::span<u8> out) {
|
||||
std::size_t done = 0;
|
||||
while (done < out.size() && position < decompressed_size) {
|
||||
const u64 id = position / block_size;
|
||||
if (id >= sizes.size()) break;
|
||||
if (cache_id != id && !Load(id)) break;
|
||||
const u64 in = position - id * block_size;
|
||||
const std::size_t n = static_cast<std::size_t>(
|
||||
std::min<u64>(out.size() - done, cache.size() - in));
|
||||
std::memcpy(out.data() + done, cache.data() + in, n);
|
||||
done += n;
|
||||
position += n;
|
||||
}
|
||||
return done;
|
||||
}
|
||||
|
||||
private:
|
||||
bool Load(u64 id) {
|
||||
u64 want = block_size;
|
||||
if (id + 1 == sizes.size()) {
|
||||
const u64 rem = decompressed_size % block_size;
|
||||
if (rem > 0) want = rem; // avoid the 0-size bug from the original nsz
|
||||
}
|
||||
try { cache.resize(static_cast<std::size_t>(want)); }
|
||||
catch (const std::bad_alloc&) { return false; }
|
||||
const u32 csize = sizes[id];
|
||||
if (csize < want) {
|
||||
const auto comp = source->ReadBytes(csize, offsets[id]);
|
||||
if (comp.size() != csize ||
|
||||
Common::Compression::DecompressDataZSTD(comp, cache) != want) return false;
|
||||
} else if (source->Read(cache.data(), cache.size(), offsets[id]) != cache.size()) {
|
||||
return false;
|
||||
}
|
||||
cache_id = id;
|
||||
return true;
|
||||
}
|
||||
|
||||
FileSys::VirtualFile source;
|
||||
u64 block_size;
|
||||
std::vector<u64> offsets;
|
||||
std::vector<u32> sizes;
|
||||
u64 decompressed_size;
|
||||
u64 cache_id = ~0ULL;
|
||||
std::vector<u8> cache;
|
||||
};
|
||||
|
||||
class BlockNczFile final : public FileSys::VfsFile {
|
||||
public:
|
||||
BlockNczFile(std::string name_, std::vector<u8> hdr_, std::vector<Section> sections_,
|
||||
std::unique_ptr<BlockReader> body_)
|
||||
: name{std::move(name_)}, hdr{std::move(hdr_)}, sections{std::move(sections_)},
|
||||
body{std::move(body_)} {
|
||||
nca_size = HeaderSize;
|
||||
for (const auto& s : sections)
|
||||
nca_size = std::max<u64>(nca_size, static_cast<u64>(s.offset + s.size));
|
||||
ciphers.reserve(sections.size());
|
||||
for (const auto& s : sections)
|
||||
ciphers.push_back(s.Ctr() ? std::make_unique<AESCipher<Key128>>(s.key, Mode::CTR)
|
||||
: nullptr);
|
||||
}
|
||||
|
||||
std::string GetName() const override { return name; }
|
||||
std::size_t GetSize() const override { return static_cast<std::size_t>(nca_size); }
|
||||
bool Resize(std::size_t) override { return false; }
|
||||
FileSys::VirtualDir GetContainingDirectory() const override { return nullptr; }
|
||||
bool IsWritable() const override { return false; }
|
||||
bool IsReadable() const override { return true; }
|
||||
|
||||
std::size_t Read(u8* data, std::size_t len, std::size_t off) const override {
|
||||
if (off >= nca_size) return 0;
|
||||
len = static_cast<std::size_t>(std::min<u64>(len, nca_size - off));
|
||||
const u64 end = off + len;
|
||||
u64 pos = off;
|
||||
if (pos < HeaderSize) {
|
||||
const u64 n = std::min<u64>(end, HeaderSize) - pos;
|
||||
std::memcpy(data, hdr.data() + pos, static_cast<std::size_t>(n));
|
||||
pos += n;
|
||||
}
|
||||
std::scoped_lock lock{mutex};
|
||||
for (std::size_t i = 0; i < sections.size() && pos < end; ++i) {
|
||||
const auto& s = sections[i];
|
||||
const u64 begin = std::max<u64>(static_cast<u64>(s.offset), HeaderSize);
|
||||
const u64 stop = std::max<u64>(static_cast<u64>(s.offset + s.size), begin);
|
||||
if (stop <= pos) continue;
|
||||
if (pos < begin) {
|
||||
const u64 g = std::min(end, begin);
|
||||
std::memset(data + (pos - off), 0, static_cast<std::size_t>(g - pos));
|
||||
pos = g;
|
||||
if (pos >= end) break;
|
||||
}
|
||||
const u64 pe = std::min(end, stop);
|
||||
const std::span<u8> piece(data + (pos - off), static_cast<std::size_t>(pe - pos));
|
||||
if (body->ReadAt(pos - HeaderSize, piece) != piece.size())
|
||||
return static_cast<std::size_t>(pos - off);
|
||||
if (ciphers[i]) {
|
||||
Key128 iv{};
|
||||
SetIv(iv, s.counter, pos);
|
||||
ciphers[i]->SetIV(iv);
|
||||
ciphers[i]->Transcode(piece.data(), piece.size(), piece.data(), Op::Encrypt);
|
||||
}
|
||||
pos = pe;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
std::size_t Write(const u8*, std::size_t, std::size_t) override { return 0; }
|
||||
bool Rename(std::string_view) override { return false; }
|
||||
|
||||
private:
|
||||
const std::string name;
|
||||
const std::vector<u8> hdr;
|
||||
const std::vector<Section> sections;
|
||||
const std::unique_ptr<BlockReader> body;
|
||||
std::vector<std::unique_ptr<AESCipher<Key128>>> ciphers;
|
||||
mutable std::mutex mutex;
|
||||
u64 nca_size{};
|
||||
};
|
||||
|
||||
// Solid NCZ: one zstd stream over the whole body. Decoded incrementally into memory, up to the
|
||||
// requested offset, so sequential install reads are O(n) instead of O(n^2).
|
||||
class SolidNczFile final : public FileSys::VfsFile {
|
||||
public:
|
||||
SolidNczFile(std::string name_, FileSys::VirtualFile source_,
|
||||
std::vector<Section> sections_, u64 payload_start_)
|
||||
: name{std::move(name_)}, source{std::move(source_)}, sections{std::move(sections_)},
|
||||
payload_start{payload_start_} {
|
||||
const auto raw_header = source->ReadBytes(HeaderSize, 0);
|
||||
data.assign(raw_header.begin(), raw_header.end());
|
||||
frontier = HeaderSize;
|
||||
}
|
||||
|
||||
~SolidNczFile() override {
|
||||
if (stream != nullptr) ZSTD_freeDStream(stream);
|
||||
}
|
||||
|
||||
std::string GetName() const override { return name; }
|
||||
std::size_t GetSize() const override {
|
||||
u64 size = HeaderSize;
|
||||
for (const auto& s : sections)
|
||||
size = std::max<u64>(size, static_cast<u64>(s.offset + s.size));
|
||||
return static_cast<std::size_t>(size);
|
||||
}
|
||||
bool Resize(std::size_t) override { return false; }
|
||||
FileSys::VirtualDir GetContainingDirectory() const override { return nullptr; }
|
||||
bool IsWritable() const override { return false; }
|
||||
bool IsReadable() const override { return true; }
|
||||
|
||||
std::size_t Read(u8* out, std::size_t len, std::size_t off) const override {
|
||||
std::scoped_lock lock{mutex};
|
||||
const u64 nca_size = GetSize();
|
||||
if (off >= nca_size) return 0;
|
||||
len = static_cast<std::size_t>(std::min<u64>(len, nca_size - off));
|
||||
const u64 needed = off + len;
|
||||
if (needed > frontier && !DecodeTo(needed)) return 0;
|
||||
std::memcpy(out, data.data() + off, len);
|
||||
return len;
|
||||
}
|
||||
|
||||
std::size_t Write(const u8*, std::size_t, std::size_t) override { return 0; }
|
||||
bool Rename(std::string_view) override { return false; }
|
||||
|
||||
private:
|
||||
bool DecodeTo(u64 needed) const {
|
||||
if (stream == nullptr) {
|
||||
stream = ZSTD_createDStream();
|
||||
if (stream == nullptr) return false;
|
||||
ZSTD_DCtx_setParameter(stream, ZSTD_d_windowLogMax, 31);
|
||||
read_pos = payload_start;
|
||||
}
|
||||
|
||||
const u64 file_size = source->GetSize();
|
||||
std::vector<u8> out_buf(Chunk);
|
||||
|
||||
std::size_t index = 0;
|
||||
for (; index < sections.size(); ++index) {
|
||||
const auto& s = sections[index];
|
||||
const u64 begin = std::max<u64>(static_cast<u64>(s.offset), HeaderSize);
|
||||
const u64 end = std::max<u64>(static_cast<u64>(s.offset) + static_cast<u64>(s.size),
|
||||
begin);
|
||||
if (end > frontier) break;
|
||||
}
|
||||
|
||||
for (; index < sections.size() && frontier < needed; ++index) {
|
||||
const auto& s = sections[index];
|
||||
const u64 begin = std::max<u64>(static_cast<u64>(s.offset), HeaderSize);
|
||||
const u64 end = std::max<u64>(static_cast<u64>(s.offset) + static_cast<u64>(s.size),
|
||||
begin);
|
||||
if (end <= frontier) continue;
|
||||
u64 pos = std::max<u64>(begin, frontier);
|
||||
|
||||
std::optional<AESCipher<Key128>> cipher;
|
||||
if (s.Ctr()) cipher.emplace(s.key, Mode::CTR);
|
||||
|
||||
while (pos < end && frontier < needed) {
|
||||
const u64 want = std::min<u64>(end - pos, out_buf.size());
|
||||
std::size_t produced = 0;
|
||||
while (produced < want) {
|
||||
if (in.pos >= in.size) {
|
||||
const u64 remaining = file_size - read_pos;
|
||||
const std::size_t chunk = static_cast<std::size_t>(
|
||||
std::min<u64>(Chunk, remaining));
|
||||
if (chunk == 0) { LOG_ERROR(Crypto, "Solid NCZ: truncated"); return false; }
|
||||
const auto bytes = source->ReadBytes(chunk, read_pos);
|
||||
if (bytes.empty()) { LOG_ERROR(Crypto, "Solid NCZ: truncated"); return false; }
|
||||
read_pos += bytes.size();
|
||||
in_buf.assign(bytes.begin(), bytes.end());
|
||||
in.src = in_buf.data();
|
||||
in.size = in_buf.size();
|
||||
in.pos = 0;
|
||||
}
|
||||
ZSTD_outBuffer outb{out_buf.data() + produced, want - produced, 0};
|
||||
const std::size_t res = ZSTD_decompressStream(stream, &outb, &in);
|
||||
if (ZSTD_isError(res)) {
|
||||
LOG_ERROR(Crypto, "Solid NCZ: zstd error");
|
||||
return false;
|
||||
}
|
||||
produced += outb.pos;
|
||||
}
|
||||
if (cipher) {
|
||||
Key128 iv{};
|
||||
SetIv(iv, s.counter, pos);
|
||||
cipher->SetIV(iv);
|
||||
cipher->Transcode(out_buf.data(), produced, out_buf.data(), Op::Encrypt);
|
||||
}
|
||||
data.insert(data.end(), out_buf.begin(), out_buf.begin() + produced);
|
||||
pos += produced;
|
||||
frontier = pos;
|
||||
}
|
||||
}
|
||||
return frontier >= needed;
|
||||
}
|
||||
|
||||
const std::string name;
|
||||
const FileSys::VirtualFile source;
|
||||
const std::vector<Section> sections;
|
||||
const u64 payload_start;
|
||||
mutable std::mutex mutex;
|
||||
mutable std::vector<u8> data;
|
||||
mutable ZSTD_DStream* stream = nullptr;
|
||||
mutable u64 read_pos = 0;
|
||||
mutable u64 frontier = 0;
|
||||
mutable std::vector<u8> in_buf;
|
||||
mutable ZSTD_inBuffer in{nullptr, 0, 0};
|
||||
};
|
||||
|
||||
// Shared opener: block path always; solid only when allow_solid is true.
|
||||
FileSys::VirtualFile OpenNcz(const FileSys::VirtualFile& f, std::string_view name,
|
||||
bool allow_solid) {
|
||||
if (!f) return nullptr;
|
||||
u64 ps = 0;
|
||||
auto parsed = ParseHeader(f, ps);
|
||||
if (!parsed) return nullptr;
|
||||
auto& [sections, block] = *parsed;
|
||||
|
||||
if (block) {
|
||||
auto hdr = f->ReadBytes(HeaderSize, 0);
|
||||
if (hdr.size() != HeaderSize) return nullptr;
|
||||
std::unique_ptr<BlockReader> body;
|
||||
try { body = std::make_unique<BlockReader>(f, *block, ps); }
|
||||
catch (const std::exception& e) {
|
||||
LOG_ERROR(Crypto, "Failed to open NCZBLOCK: {}", e.what());
|
||||
return nullptr;
|
||||
}
|
||||
return std::make_shared<BlockNczFile>(std::string{name}, std::move(hdr),
|
||||
std::move(sections), std::move(body));
|
||||
}
|
||||
|
||||
if (allow_solid) {
|
||||
return std::make_shared<SolidNczFile>(std::string{name}, f, std::move(sections), ps);
|
||||
}
|
||||
|
||||
LOG_DEBUG(Crypto, "Solid NCZ {} skipped (only NCZBLOCK is supported for play)", name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
bool IsBlockCompressedNCZ(const FileSys::VirtualFile& f) {
|
||||
if (!f) return false;
|
||||
u64 ps = 0;
|
||||
const auto p = ParseHeader(f, ps);
|
||||
return p && p->second.has_value();
|
||||
}
|
||||
|
||||
FileSys::VirtualFile DecompressNCZ(const FileSys::VirtualFile& f, std::string_view name) {
|
||||
return OpenNcz(f, name, /*allow_solid=*/false);
|
||||
}
|
||||
|
||||
FileSys::VirtualFile DecompressNCZForInstall(const FileSys::VirtualFile& f,
|
||||
std::string_view name) {
|
||||
return OpenNcz(f, name, /*allow_solid=*/true);
|
||||
}
|
||||
|
||||
} // namespace Core::Crypto
|
||||
@@ -0,0 +1,35 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#include "core/file_sys/vfs/vfs_types.h"
|
||||
|
||||
namespace Core::Crypto {
|
||||
|
||||
/**
|
||||
* Opens an NCZBLOCK-compressed NCA (nsz -B / .xcz block format) as a VirtualFile backed by
|
||||
* on-demand block decoding. Nothing is written to disk; each read decodes only the blocks it
|
||||
* covers and re-encrypts CTR sections. Solid NCZ is not supported.
|
||||
*
|
||||
* @param ncz_file The raw NCZ payload (e.g. "<id>.ncz" entry from an NSZ/XCZ PFS0).
|
||||
* @param output_name Name to give the returned VirtualFile (e.g. "<id>.nca").
|
||||
*
|
||||
* @return a VirtualFile for the NCA, or nullptr if the NCZ is malformed or not NCZBLOCK.
|
||||
*/
|
||||
[[nodiscard]] FileSys::VirtualFile DecompressNCZ(const FileSys::VirtualFile& ncz_file,
|
||||
std::string_view output_name);
|
||||
|
||||
/**
|
||||
* Returns true if the given file is an NCZ payload in block-compressed (NCZBLOCK) form,
|
||||
* i.e. one that DecompressNCZ() can open. Solid NCZ returns false.
|
||||
*/
|
||||
[[nodiscard]] bool IsBlockCompressedNCZ(const FileSys::VirtualFile& ncz_file);
|
||||
|
||||
// Install to NAND: same as DecompressNCZ, but solid NCZ is decoded on demand too.
|
||||
[[nodiscard]] FileSys::VirtualFile DecompressNCZForInstall(
|
||||
const FileSys::VirtualFile& ncz_file, std::string_view output_name);
|
||||
|
||||
} // namespace Core::Crypto
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "common/random.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/crypto/ncz_decompressor.h"
|
||||
#include "core/file_sys/card_image.h"
|
||||
#include "core/file_sys/common_funcs.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
@@ -158,6 +159,25 @@ static std::shared_ptr<NSP> OpenContainerAsNsp(const VirtualFile& file, Loader::
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Checks that every .ncz entry in the container is NCZBLOCK. Solid NCZ has no random access,
|
||||
// and the whole point of this build is to reject it at scan time so such files never appear
|
||||
// in the game list. Containers without any .ncz (plain NSP/XCI) pass through.
|
||||
static bool ContainerIsFullyBlockCompressed(const std::shared_ptr<NSP>& nsp) {
|
||||
if (!nsp) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& entry : nsp->GetFiles()) {
|
||||
const auto name = entry->GetName();
|
||||
if (name.size() < 4 || name.substr(name.size() - 4) != ".ncz") {
|
||||
continue;
|
||||
}
|
||||
if (!Core::Crypto::IsBlockCompressedNCZ(entry)) {
|
||||
return false; // solid NCZ -> reject the whole container
|
||||
}
|
||||
}
|
||||
return true; // no .ncz at all, or all block-compressed
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
bool ForEachContainerEntry(const std::shared_ptr<NSP>& nsp, bool only_content,
|
||||
std::optional<u64> base_program_id, Callback&& on_entry) {
|
||||
@@ -811,7 +831,14 @@ std::vector<ContentProviderEntry> RegisteredCache::ListEntriesFilter(
|
||||
}
|
||||
|
||||
static std::shared_ptr<NCA> GetNCAFromNSPForID(const NSP& nsp, const NcaID& id) {
|
||||
auto file = nsp.GetFile(fmt::format("{}.nca", Common::HexToString(id, false)));
|
||||
const auto id_string = Common::HexToString(id, false);
|
||||
auto file = nsp.GetFile(fmt::format("{}.nca", id_string));
|
||||
if (file == nullptr) {
|
||||
if (auto ncz_file = nsp.GetFile(fmt::format("{}.ncz", id_string))) {
|
||||
file = Core::Crypto::DecompressNCZForInstall(
|
||||
ncz_file, fmt::format("{}.nca", id_string));
|
||||
}
|
||||
}
|
||||
if (file == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1273,6 +1300,12 @@ bool ManualContentProvider::AddEntriesFromContainer(VirtualFile file, bool only_
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject solid NCZ (.nsz without -B): only block-compressed NCZ is supported.
|
||||
if (!ContainerIsFullyBlockCompressed(nsp)) {
|
||||
LOG_DEBUG(Service_FS, "Skipping solid-compressed container: {}", file->GetName());
|
||||
return false;
|
||||
}
|
||||
|
||||
return ForEachContainerEntry(
|
||||
nsp, only_content, base_program_id,
|
||||
[this](TitleType title_type, ContentRecordType content_type, u64 title_id,
|
||||
@@ -1416,9 +1449,9 @@ void ExternalContentProvider::ScanDirectory(const VirtualDir& dir) {
|
||||
|
||||
const auto extension = Common::ToLower(filename.substr(dot_pos + 1));
|
||||
|
||||
if (extension == "nsp") {
|
||||
if (extension == "nsp" || extension == "nsz") {
|
||||
ProcessNSP(file);
|
||||
} else if (extension == "xci") {
|
||||
} else if (extension == "xci" || extension == "xcz") {
|
||||
ProcessXCI(file);
|
||||
}
|
||||
}
|
||||
@@ -1434,6 +1467,12 @@ void ExternalContentProvider::ProcessNSP(const VirtualFile& file) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reject solid NCZ (.nsz without -B): Eden only supports block-compressed NCZ.
|
||||
if (!ContainerIsFullyBlockCompressed(nsp)) {
|
||||
LOG_DEBUG(Service_FS, "Skipping solid-compressed container: {}", file->GetName());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DEBUG(Service_FS, "Processing NSP file: {}", file->GetName());
|
||||
AddExternalEntriesFromContainer(nsp, entries, versions, multi_version_entries);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "common/hex_util.h"
|
||||
#include "common/logging.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/crypto/ncz_decompressor.h"
|
||||
#include "core/file_sys/common_funcs.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/nca_metadata.h"
|
||||
@@ -246,6 +247,17 @@ void NSP::ReadNCAs(const std::vector<VirtualFile>& files) {
|
||||
const auto id_string = Common::HexToString(rec.nca_id, false);
|
||||
auto next_file = pfs->GetFile(fmt::format("{}.nca", id_string));
|
||||
|
||||
if (next_file == nullptr) {
|
||||
auto ncz_file = pfs->GetFile(fmt::format("{}.ncz", id_string));
|
||||
if (ncz_file != nullptr) {
|
||||
next_file = Core::Crypto::DecompressNCZ(ncz_file, fmt::format("{}.nca", id_string));
|
||||
if (next_file == nullptr) {
|
||||
LOG_DEBUG(Service_FS, "Skipping solid or unsupported NCZ content {}.ncz", id_string);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (next_file == nullptr) {
|
||||
if (rec.type != ContentRecordType::DeltaFragment) {
|
||||
LOG_WARNING(Service_FS,
|
||||
|
||||
@@ -82,12 +82,12 @@ std::shared_ptr<FileSys::NSP> OpenContainerAsNsp(FileSys::VirtualFile file, File
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (type == FileType::NSP) {
|
||||
if (type == FileType::NSP || type == FileType::NSZ) {
|
||||
auto nsp = std::make_shared<FileSys::NSP>(file, program_id, program_index);
|
||||
return nsp->GetStatus() == ResultStatus::Success ? nsp : nullptr;
|
||||
}
|
||||
|
||||
if (type == FileType::XCI) {
|
||||
if (type == FileType::XCI || type == FileType::XCZ) {
|
||||
FileSys::XCI xci{file, program_id, program_index};
|
||||
if (xci.GetStatus() != ResultStatus::Success) {
|
||||
return nullptr;
|
||||
@@ -144,7 +144,8 @@ FileType IdentifyFile(FileSys::VirtualFile file) {
|
||||
}
|
||||
|
||||
bool IsContainerType(FileType type) {
|
||||
return type == FileType::NSP || type == FileType::XCI;
|
||||
return type == FileType::NSP || type == FileType::NSZ || type == FileType::XCI ||
|
||||
type == FileType::XCZ;
|
||||
}
|
||||
|
||||
bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type, u64 program_id,
|
||||
@@ -180,8 +181,12 @@ FileType GuessFromFilename(const std::string& name) {
|
||||
return FileType::NCA;
|
||||
else if (extension == "xci")
|
||||
return FileType::XCI;
|
||||
else if (extension == "xcz")
|
||||
return FileType::XCZ;
|
||||
else if (extension == "nsp")
|
||||
return FileType::NSP;
|
||||
else if (extension == "nsz")
|
||||
return FileType::NSZ;
|
||||
else if (extension == "kip")
|
||||
return FileType::KIP;
|
||||
return FileType::Unknown;
|
||||
@@ -197,10 +202,14 @@ std::string GetFileTypeString(FileType type) {
|
||||
return "NCA";
|
||||
case FileType::XCI:
|
||||
return "XCI";
|
||||
case FileType::XCZ:
|
||||
return "XCZ";
|
||||
case FileType::NAX:
|
||||
return "NAX";
|
||||
case FileType::NSP:
|
||||
return "NSP";
|
||||
case FileType::NSZ:
|
||||
return "NSZ";
|
||||
case FileType::KIP:
|
||||
return "KIP";
|
||||
case FileType::DeconstructedRomDirectory:
|
||||
@@ -322,6 +331,7 @@ static std::unique_ptr<AppLoader> GetFileLoader(Core::System& system, FileSys::V
|
||||
|
||||
// NX XCI (nX Card Image) file format.
|
||||
case FileType::XCI:
|
||||
case FileType::XCZ:
|
||||
return std::make_unique<AppLoader_XCI>(std::move(file), system.GetFileSystemController(),
|
||||
system.GetContentProvider(), program_id,
|
||||
program_index);
|
||||
@@ -332,6 +342,7 @@ static std::unique_ptr<AppLoader> GetFileLoader(Core::System& system, FileSys::V
|
||||
|
||||
// NX NSP (Nintendo Submission Package) file format
|
||||
case FileType::NSP:
|
||||
case FileType::NSZ:
|
||||
return std::make_unique<AppLoader_NSP>(std::move(file), system.GetFileSystemController(),
|
||||
system.GetContentProvider(), program_id,
|
||||
program_index);
|
||||
|
||||
@@ -42,7 +42,9 @@ enum class FileType {
|
||||
NRO,
|
||||
NCA,
|
||||
NSP,
|
||||
NSZ,
|
||||
XCI,
|
||||
XCZ,
|
||||
NAX,
|
||||
KIP,
|
||||
DeconstructedRomDirectory,
|
||||
|
||||
@@ -56,6 +56,11 @@ AppLoader_NSP::AppLoader_NSP(FileSys::VirtualFile file_,
|
||||
AppLoader_NSP::~AppLoader_NSP() = default;
|
||||
|
||||
FileType AppLoader_NSP::IdentifyType(const FileSys::VirtualFile& nsp_file) {
|
||||
// .nsz is NSP-compatible; return NSZ so the game list shows the right type.
|
||||
if (nsp_file->GetName().ends_with(".nsz")) {
|
||||
return FileType::NSZ;
|
||||
}
|
||||
|
||||
const FileSys::NSP nsp(nsp_file);
|
||||
|
||||
if (nsp.GetStatus() != ResultStatus::Success) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||
@@ -163,7 +163,9 @@ inline InstallResult InstallNSP(Core::System& system, FileSys::VfsFilesystem& vf
|
||||
|
||||
std::shared_ptr<FileSys::NSP> nsp;
|
||||
FileSys::VirtualFile file = vfs.OpenFile(filename, FileSys::OpenMode::Read);
|
||||
if (boost::to_lower_copy(file->GetName()).ends_with("nsp")) {
|
||||
const auto lower_name = boost::to_lower_copy(file->GetName());
|
||||
|
||||
if (lower_name.ends_with("nsp") || lower_name.ends_with("nsz")) {
|
||||
nsp = std::make_shared<FileSys::NSP>(file);
|
||||
if (nsp->IsExtractedType()) {
|
||||
return InstallResult::Failure;
|
||||
|
||||
@@ -360,7 +360,8 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
|
||||
}
|
||||
|
||||
if (target == ScanTarget::PopulateGameList &&
|
||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP) &&
|
||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::XCZ ||
|
||||
file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ) &&
|
||||
!Loader::IsBootableGameContainer(file, file_type)) {
|
||||
return true;
|
||||
}
|
||||
@@ -375,7 +376,9 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
|
||||
program_id, file);
|
||||
} else if (Settings::values.ext_content_from_game_dirs.GetValue() &&
|
||||
(file_type == Loader::FileType::XCI ||
|
||||
file_type == Loader::FileType::NSP)) {
|
||||
file_type == Loader::FileType::XCZ ||
|
||||
file_type == Loader::FileType::NSP ||
|
||||
file_type == Loader::FileType::NSZ)) {
|
||||
void(provider->AddEntriesFromContainer(file));
|
||||
}
|
||||
} else {
|
||||
@@ -402,7 +405,8 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
|
||||
};
|
||||
|
||||
if (res2 == Loader::ResultStatus::Success && program_ids.size() > 1 &&
|
||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
|
||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::XCZ ||
|
||||
file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ)) {
|
||||
for (const auto id : program_ids) {
|
||||
// dravee suggested this, only viable way to
|
||||
// not show sub-games in qlaunch for now.
|
||||
|
||||
@@ -57,7 +57,8 @@ std::unique_ptr<EmuThread> emu_thread = nullptr;
|
||||
|
||||
const QStringList supported_file_extensions = {QStringLiteral("nro"), QStringLiteral("nso"),
|
||||
QStringLiteral("nca"), QStringLiteral("xci"),
|
||||
QStringLiteral("nsp"), QStringLiteral("kip")};
|
||||
QStringLiteral("xcz"), QStringLiteral("nsp"),
|
||||
QStringLiteral("nsz"), QStringLiteral("kip")};
|
||||
|
||||
Core::Frontend::WindowSystemType GetWindowSystemType() {
|
||||
// Determine WSI type based on Qt platform.
|
||||
|
||||
@@ -40,10 +40,9 @@ struct HandleKey {
|
||||
u32 sec_index, sec_offset, sec_shift_left;
|
||||
u32 count;
|
||||
bool has_secondary;
|
||||
u32 mask, sec_mask;
|
||||
constexpr bool operator==(const HandleKey& o) const noexcept {
|
||||
return std::tie(index, offset, shift_left, sec_index, sec_offset, sec_shift_left, count, has_secondary, mask, sec_mask)
|
||||
== std::tie(o.index, o.offset, o.shift_left, o.sec_index, o.sec_offset, o.sec_shift_left, o.count, o.has_secondary, o.mask, o.sec_mask);
|
||||
return std::tie(index, offset, shift_left, sec_index, sec_offset, sec_shift_left, count, has_secondary)
|
||||
== std::tie(o.index, o.offset, o.shift_left, o.sec_index, o.sec_offset, o.sec_shift_left, o.count, o.has_secondary);
|
||||
}
|
||||
};
|
||||
struct HandleKeyHash {
|
||||
@@ -53,7 +52,6 @@ struct HandleKeyHash {
|
||||
h ^= (size_t(k.sec_index) << 33) ^ (size_t(k.sec_offset) << 2);
|
||||
h ^= (size_t(k.sec_shift_left) << 3);
|
||||
h ^= (size_t(k.count) << 7);
|
||||
h ^= (size_t(k.mask) << 11) ^ (size_t(k.sec_mask) << 13);
|
||||
return h ^ (k.has_secondary ? 0x9e3779b97f4a7c15ULL : 0ULL);
|
||||
}
|
||||
};
|
||||
@@ -68,14 +66,6 @@ struct ConstBufferAddr {
|
||||
IR::U32 dynamic_offset;
|
||||
u32 count;
|
||||
bool has_secondary;
|
||||
/// The dynamic offset is a constant buffer value shifted left, the same for the whole draw
|
||||
bool has_index_cbuf;
|
||||
u32 index_cbuf_index;
|
||||
u32 index_cbuf_offset;
|
||||
u32 index_shift;
|
||||
/// Applied after the shifts
|
||||
u32 mask = 0xffffffff;
|
||||
u32 secondary_mask = 0xffffffff;
|
||||
};
|
||||
|
||||
class Environment {
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
@@ -41,9 +41,9 @@ union F2I {
|
||||
BitField<39, 2, Rounding> rounding;
|
||||
BitField<41, 1, u64> half;
|
||||
BitField<44, 1, u64> ftz;
|
||||
BitField<45, 1, u64> neg;
|
||||
BitField<45, 1, u64> abs;
|
||||
BitField<47, 1, u64> cc;
|
||||
BitField<49, 1, u64> abs;
|
||||
BitField<49, 1, u64> neg;
|
||||
};
|
||||
|
||||
size_t BitSize(DestFormat dest_format) {
|
||||
|
||||
@@ -43,8 +43,7 @@ u32 DynamicDescriptorSizeShift(const IR::U32& dynamic_offset) {
|
||||
if (!shift.IsImmediate())
|
||||
return DESCRIPTOR_SIZE_SHIFT;
|
||||
const u32 size_shift = shift.U32();
|
||||
// Separate texture and sampler handles are 4 bytes apart
|
||||
return size_shift >= 2 && size_shift < 31 ? size_shift : DESCRIPTOR_SIZE_SHIFT;
|
||||
return size_shift >= DESCRIPTOR_SIZE_SHIFT && size_shift < 31 ? size_shift : DESCRIPTOR_SIZE_SHIFT;
|
||||
}
|
||||
|
||||
u32 DynamicDescriptorCount(u32 base_offset, u32 size_shift, u32 max_descriptors) {
|
||||
@@ -60,37 +59,6 @@ u32 DynamicDescriptorCount(u32 base_offset, u32 size_shift, u32 max_descriptors)
|
||||
return std::min(descriptor_limit, available_count);
|
||||
}
|
||||
|
||||
struct UniformIndex {
|
||||
u32 cbuf_index;
|
||||
u32 cbuf_offset;
|
||||
u32 shift;
|
||||
};
|
||||
|
||||
/// Matches a dynamic handle offset that is a constant buffer value, optionally shifted left
|
||||
std::optional<UniformIndex> TrackUniformIndex(const IR::U32& dynamic_offset) {
|
||||
if (dynamic_offset.IsImmediate()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const IR::Inst* inst = dynamic_offset.InstRecursive();
|
||||
u32 shift = 0;
|
||||
if (inst->GetOpcode() == IR::Opcode::ShiftLeftLogical32) {
|
||||
if (!inst->Arg(1).IsImmediate() || inst->Arg(0).IsImmediate()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
shift = inst->Arg(1).U32();
|
||||
inst = inst->Arg(0).InstRecursive();
|
||||
}
|
||||
if (inst->GetOpcode() != IR::Opcode::GetCbufU32 || !inst->Arg(0).IsImmediate() ||
|
||||
!inst->Arg(1).IsImmediate() || shift >= 32) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return UniformIndex{
|
||||
.cbuf_index = inst->Arg(0).U32(),
|
||||
.cbuf_offset = inst->Arg(1).U32(),
|
||||
.shift = shift,
|
||||
};
|
||||
}
|
||||
|
||||
u32 SaturatingSub(u32 lhs, u32 rhs) {
|
||||
return lhs > rhs ? lhs - rhs : 0;
|
||||
}
|
||||
@@ -305,15 +273,14 @@ static inline u32 GetTextureHandleCached(Environment& env, const ConstBufferAddr
|
||||
// If has secondary, then it will be cbuf.secondary_{index|offset}, else its 0.
|
||||
// So we can just hand it out the raw variable without using sec_idx or sec_off
|
||||
// because comparing 0 against 0 will yield true.
|
||||
const HandleKey hk{cbuf.index, cbuf.offset, cbuf.shift_left, cbuf.secondary_index, cbuf.secondary_offset, cbuf.secondary_shift_left, cbuf.count, cbuf.has_secondary, cbuf.mask, cbuf.secondary_mask};
|
||||
const HandleKey hk{cbuf.index, cbuf.offset, cbuf.shift_left, cbuf.secondary_index, cbuf.secondary_offset, cbuf.secondary_shift_left, cbuf.count, cbuf.has_secondary};
|
||||
if (auto it = env.handle_cache.find(hk); it != env.handle_cache.end())
|
||||
return it->second;
|
||||
u32 handle = (ReadCbufCached(env, cbuf.index, cbuf.offset) << cbuf.shift_left) & cbuf.mask;
|
||||
if (cbuf.has_secondary) {
|
||||
handle |= (ReadCbufCached(env, cbuf.secondary_index, cbuf.secondary_offset)
|
||||
<< cbuf.secondary_shift_left) &
|
||||
cbuf.secondary_mask;
|
||||
}
|
||||
const u32 sec_idx = cbuf.has_secondary ? cbuf.secondary_index : cbuf.index;
|
||||
const u32 sec_off = cbuf.has_secondary ? cbuf.secondary_offset : cbuf.offset;
|
||||
const u32 lhs = ReadCbufCached(env, cbuf.index, cbuf.offset) << cbuf.shift_left;
|
||||
const u32 rhs = ReadCbufCached(env, sec_idx, sec_off) << cbuf.secondary_shift_left;
|
||||
const u32 handle = lhs | rhs;
|
||||
env.handle_cache.emplace(hk, handle);
|
||||
return handle;
|
||||
}
|
||||
@@ -385,22 +352,10 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
|
||||
if (lhs->has_secondary || rhs->has_secondary) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (lhs->count > 1 && rhs->count > 1) {
|
||||
if (lhs->count > 1 || rhs->count > 1) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (rhs->has_index_cbuf || lhs->has_index_cbuf) {
|
||||
if (rhs->has_index_cbuf && lhs->has_index_cbuf) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (rhs->has_index_cbuf) {
|
||||
std::swap(lhs, rhs);
|
||||
}
|
||||
} else if (rhs->count > 1) {
|
||||
// An indexed handle array combined with a single handle (e.g. texture indices with
|
||||
// a shared sampler), only the primary handle is indexed
|
||||
std::swap(lhs, rhs);
|
||||
} else if (lhs->count == 1 &&
|
||||
(lhs->shift_left > 0 || lhs->index > rhs->index || lhs->offset > rhs->offset)) {
|
||||
if (lhs->shift_left > 0 || lhs->index > rhs->index || lhs->offset > rhs->offset) {
|
||||
std::swap(lhs, rhs);
|
||||
}
|
||||
return ConstBufferAddr{
|
||||
@@ -410,15 +365,9 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
|
||||
.secondary_index = rhs->index,
|
||||
.secondary_offset = rhs->offset,
|
||||
.secondary_shift_left = rhs->shift_left,
|
||||
.dynamic_offset = lhs->dynamic_offset,
|
||||
.count = lhs->count,
|
||||
.dynamic_offset = {},
|
||||
.count = 1,
|
||||
.has_secondary = true,
|
||||
.has_index_cbuf = lhs->has_index_cbuf,
|
||||
.index_cbuf_index = lhs->index_cbuf_index,
|
||||
.index_cbuf_offset = lhs->index_cbuf_offset,
|
||||
.index_shift = lhs->index_shift,
|
||||
.mask = lhs->mask,
|
||||
.secondary_mask = rhs->mask,
|
||||
};
|
||||
}
|
||||
case IR::Opcode::ShiftLeftLogical32: {
|
||||
@@ -429,8 +378,6 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
|
||||
std::optional lhs{TrackCached(inst->Arg(0), env, host_info)};
|
||||
if (lhs) {
|
||||
lhs->shift_left = shift.U32();
|
||||
// A mask applied before the shift moves with the value
|
||||
lhs->mask <<= shift.U32();
|
||||
}
|
||||
return lhs;
|
||||
}
|
||||
@@ -458,7 +405,7 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
|
||||
}
|
||||
std::optional lhs{TrackCached(op1, env, host_info)};
|
||||
if (lhs) {
|
||||
lhs->mask &= op2.U32();
|
||||
lhs->shift_left = static_cast<u32>(std::countr_zero(op2.U32()));
|
||||
}
|
||||
return lhs;
|
||||
break;
|
||||
@@ -488,12 +435,12 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
|
||||
};
|
||||
}
|
||||
IR::Inst* const offset_inst{offset.InstRecursive()};
|
||||
if (offset_inst->GetOpcode() != IR::Opcode::IAdd32) {
|
||||
return std::nullopt;
|
||||
}
|
||||
u32 base_offset{};
|
||||
IR::U32 dynamic_offset;
|
||||
if (offset_inst->GetOpcode() != IR::Opcode::IAdd32) {
|
||||
// The handle array starts at the beginning of the constant buffer
|
||||
dynamic_offset = IR::U32{offset};
|
||||
} else if (offset_inst->Arg(0).IsImmediate()) {
|
||||
if (offset_inst->Arg(0).IsImmediate()) {
|
||||
base_offset = offset_inst->Arg(0).U32();
|
||||
dynamic_offset = IR::U32{offset_inst->Arg(1)};
|
||||
} else if (offset_inst->Arg(1).IsImmediate()) {
|
||||
@@ -503,7 +450,6 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
|
||||
return std::nullopt;
|
||||
}
|
||||
auto const size_shift = DynamicDescriptorSizeShift(dynamic_offset);
|
||||
const std::optional<UniformIndex> uniform_index{TrackUniformIndex(dynamic_offset)};
|
||||
auto const sampled_limit = (std::max)(1U, (std::min)(host_info.max_per_stage_descriptor_sampled_images,
|
||||
host_info.max_descriptor_set_sampled_images));
|
||||
auto const resource_limit = (std::max)(1U, host_info.max_per_stage_resources);
|
||||
@@ -517,10 +463,6 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
|
||||
.dynamic_offset = dynamic_offset,
|
||||
.count = DynamicDescriptorCount(base_offset, size_shift, (std::min)({DESCRIPTOR_MAX_COUNT, sampled_limit, resource_limit})),
|
||||
.has_secondary = false,
|
||||
.has_index_cbuf = uniform_index.has_value(),
|
||||
.index_cbuf_index = uniform_index ? uniform_index->cbuf_index : 0,
|
||||
.index_cbuf_offset = uniform_index ? uniform_index->cbuf_offset : 0,
|
||||
.index_shift = uniform_index ? uniform_index->shift : 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -555,13 +497,12 @@ TextureInst MakeInst(Environment& env, IR::Block* block, IR::Inst& inst, const H
|
||||
}
|
||||
|
||||
u32 GetTextureHandle(Environment& env, const ConstBufferAddr& cbuf) {
|
||||
u32 handle{(env.ReadCbufValue(cbuf.index, cbuf.offset) << cbuf.shift_left) & cbuf.mask};
|
||||
if (cbuf.has_secondary) {
|
||||
handle |= (env.ReadCbufValue(cbuf.secondary_index, cbuf.secondary_offset)
|
||||
<< cbuf.secondary_shift_left) &
|
||||
cbuf.secondary_mask;
|
||||
}
|
||||
return handle;
|
||||
const u32 secondary_index{cbuf.has_secondary ? cbuf.secondary_index : cbuf.index};
|
||||
const u32 secondary_offset{cbuf.has_secondary ? cbuf.secondary_offset : cbuf.offset};
|
||||
const u32 lhs_raw{env.ReadCbufValue(cbuf.index, cbuf.offset) << cbuf.shift_left};
|
||||
const u32 rhs_raw{env.ReadCbufValue(secondary_index, secondary_offset)
|
||||
<< cbuf.secondary_shift_left};
|
||||
return lhs_raw | rhs_raw;
|
||||
}
|
||||
|
||||
[[maybe_unused]] TextureType ReadTextureType(Environment& env, const ConstBufferAddr& cbuf) {
|
||||
@@ -595,8 +536,7 @@ public:
|
||||
desc.secondary_cbuf_offset == existing.secondary_cbuf_offset &&
|
||||
desc.secondary_shift_left == existing.secondary_shift_left &&
|
||||
desc.count == existing.count && desc.size_shift == existing.size_shift &&
|
||||
desc.has_secondary == existing.has_secondary && desc.mask == existing.mask &&
|
||||
desc.secondary_mask == existing.secondary_mask;
|
||||
desc.has_secondary == existing.has_secondary;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -622,12 +562,7 @@ public:
|
||||
desc.secondary_cbuf_index == existing.secondary_cbuf_index &&
|
||||
desc.secondary_cbuf_offset == existing.secondary_cbuf_offset &&
|
||||
desc.secondary_shift_left == existing.secondary_shift_left &&
|
||||
desc.count == existing.count && desc.size_shift == existing.size_shift &&
|
||||
desc.has_index_cbuf == existing.has_index_cbuf &&
|
||||
desc.index_cbuf_index == existing.index_cbuf_index &&
|
||||
desc.index_cbuf_offset == existing.index_cbuf_offset &&
|
||||
desc.index_shift == existing.index_shift && desc.mask == existing.mask &&
|
||||
desc.secondary_mask == existing.secondary_mask;
|
||||
desc.count == existing.count && desc.size_shift == existing.size_shift;
|
||||
})};
|
||||
// TODO: Read this from TIC
|
||||
texture_descriptors[index].is_multisample |= desc.is_multisample;
|
||||
@@ -873,12 +808,9 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
|
||||
.secondary_shift_left = cbuf.secondary_shift_left,
|
||||
.count = count,
|
||||
.size_shift = size_shift,
|
||||
.mask = cbuf.mask,
|
||||
.secondary_mask = cbuf.secondary_mask,
|
||||
});
|
||||
} else {
|
||||
// A uniform index is resolved when drawing, so a single descriptor is enough
|
||||
count = cbuf.has_index_cbuf ? 1 : std::min(count, sampled_dynamic_cap);
|
||||
count = std::min(count, sampled_dynamic_cap);
|
||||
const bool is_integer{is_texture_pixel_format_integer(cbuf)};
|
||||
index = descriptors.Add(TextureDescriptor{
|
||||
.type = flags.type,
|
||||
@@ -894,12 +826,6 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
|
||||
.secondary_shift_left = cbuf.secondary_shift_left,
|
||||
.count = count,
|
||||
.size_shift = size_shift,
|
||||
.has_index_cbuf = cbuf.has_index_cbuf,
|
||||
.index_cbuf_index = cbuf.index_cbuf_index,
|
||||
.index_cbuf_offset = cbuf.index_cbuf_offset,
|
||||
.index_shift = cbuf.index_shift,
|
||||
.mask = cbuf.mask,
|
||||
.secondary_mask = cbuf.secondary_mask,
|
||||
});
|
||||
flags.is_integer.Assign(is_integer ? 1 : 0);
|
||||
}
|
||||
|
||||
@@ -186,8 +186,6 @@ struct TextureBufferDescriptor {
|
||||
u32 secondary_shift_left;
|
||||
u32 count;
|
||||
u32 size_shift;
|
||||
u32 mask = 0xffffffff;
|
||||
u32 secondary_mask = 0xffffffff;
|
||||
|
||||
auto operator<=>(const TextureBufferDescriptor&) const = default;
|
||||
};
|
||||
@@ -221,13 +219,6 @@ struct TextureDescriptor {
|
||||
u32 secondary_shift_left;
|
||||
u32 count;
|
||||
u32 size_shift;
|
||||
/// The handle offset is moved by a constant buffer value read when drawing
|
||||
bool has_index_cbuf;
|
||||
u32 index_cbuf_index;
|
||||
u32 index_cbuf_offset;
|
||||
u32 index_shift;
|
||||
u32 mask = 0xffffffff;
|
||||
u32 secondary_mask = 0xffffffff;
|
||||
|
||||
auto operator<=>(const TextureDescriptor&) const = default;
|
||||
};
|
||||
|
||||
@@ -948,14 +948,9 @@ void BufferCache<P>::BindHostGraphicsUniformBuffer(size_t stage, u32 index, u32
|
||||
return alignment > 1 && (offset % alignment) != 0;
|
||||
}
|
||||
}();
|
||||
// Uniform data rewritten by the CPU between draws would need a transfer copy into the cached
|
||||
// buffer, which ends the current render pass. Read it from the stream buffer instead.
|
||||
static constexpr u32 MAX_STREAMED_UNIFORM_BYTES = static_cast<u32>(16_KiB);
|
||||
const bool use_fast_buffer = needs_alignment_stream
|
||||
|| (has_host_buffer && !memory_tracker.IsRegionGpuModified(device_addr, size)
|
||||
&& (size <= channel_state->uniform_buffer_skip_cache_size
|
||||
|| (size <= MAX_STREAMED_UNIFORM_BYTES
|
||||
&& memory_tracker.IsRegionCpuModified(device_addr, size))));
|
||||
|| (has_host_buffer && size <= channel_state->uniform_buffer_skip_cache_size
|
||||
&& !memory_tracker.IsRegionGpuModified(device_addr, size));
|
||||
if (use_fast_buffer) {
|
||||
if constexpr (IS_OPENGL) {
|
||||
if (runtime.HasFastBufferSubData()) {
|
||||
|
||||
@@ -259,23 +259,6 @@ public:
|
||||
|
||||
void BindHostComputeBuffers();
|
||||
|
||||
/// Calls func(buffer, offset, device_addr, size) for each storage buffer the current compute
|
||||
/// dispatch writes through a single host buffer
|
||||
template <typename Func>
|
||||
void ForEachWrittenComputeStorageBuffer(Func&& func) {
|
||||
const u32 written = channel_state->enabled_compute_storage_buffers &
|
||||
channel_state->written_compute_storage_buffers;
|
||||
ForEachEnabledBit(written, [&](u32 index) {
|
||||
const Binding& binding = channel_state->compute_storage_buffers[index];
|
||||
if (binding.buffer_id == NULL_BUFFER_ID || binding.segment_count != 0 ||
|
||||
binding.size == 0) {
|
||||
return;
|
||||
}
|
||||
Buffer& buffer = slot_buffers[binding.buffer_id];
|
||||
func(buffer, buffer.Offset(binding.device_addr), binding.device_addr, binding.size);
|
||||
});
|
||||
}
|
||||
|
||||
void SetUniformBuffersState(const std::array<u32, NUM_STAGES>& mask,
|
||||
const UniformBufferSizes* sizes);
|
||||
|
||||
|
||||
@@ -83,7 +83,6 @@ set(SHADER_FILES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_present.vert
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_present_scaleforce_fp16.frag
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_present_scaleforce_fp32.frag
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_block_linear_unswizzle_2d_buffer.comp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_quad_indexed.comp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_turbo_mode.comp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_uint8.comp
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#version 460 core
|
||||
|
||||
// Copies a block linear 2D image from a buffer into a tightly packed linear buffer, 4 bytes at a
|
||||
// time. 16 byte chunks of a row are contiguous in a GOB, so any texel size works as long as the
|
||||
// row size is a multiple of 4 bytes.
|
||||
|
||||
layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout(std430, set = 0, binding = 0) readonly buffer InputBuffer {
|
||||
uint swizzled[];
|
||||
};
|
||||
|
||||
layout(std430, set = 0, binding = 1) writeonly buffer OutputBuffer {
|
||||
uint linear[];
|
||||
};
|
||||
|
||||
layout(push_constant) uniform PushConstants {
|
||||
uint src_word_offset;
|
||||
uint row_words;
|
||||
uint height;
|
||||
uint layer_stride;
|
||||
uint block_size;
|
||||
uint x_shift;
|
||||
uint block_height;
|
||||
uint block_height_mask;
|
||||
};
|
||||
|
||||
const uint GOB_SIZE_X_SHIFT = 6;
|
||||
const uint GOB_SIZE_Y_SHIFT = 3;
|
||||
const uint GOB_SIZE_SHIFT = GOB_SIZE_X_SHIFT + GOB_SIZE_Y_SHIFT;
|
||||
|
||||
uint SwizzleTable(uint pos) {
|
||||
const uint t[8] = uint[](
|
||||
0x12100200, 0x13110301, 0x16140604, 0x17150705,
|
||||
0x1a180a08, 0x1b190b09, 0x1e1c0e0c, 0x1f1d0f0d
|
||||
);
|
||||
const uint i = pos >> 4;
|
||||
const uint h = (t[i / 4] >> ((i % 4) * 8)) & 0xff;
|
||||
return (h << 4) | (pos & 0xf);
|
||||
}
|
||||
|
||||
void main() {
|
||||
const uint word = gl_GlobalInvocationID.x;
|
||||
const uint y = gl_GlobalInvocationID.y;
|
||||
const uint layer = gl_GlobalInvocationID.z;
|
||||
if (word >= row_words || y >= height) {
|
||||
return;
|
||||
}
|
||||
const uint x = word * 4;
|
||||
const uint block_y = y >> GOB_SIZE_Y_SHIFT;
|
||||
|
||||
uint offset = layer * layer_stride;
|
||||
offset += (block_y >> block_height) * block_size;
|
||||
offset += (block_y & block_height_mask) << GOB_SIZE_SHIFT;
|
||||
offset += (x >> GOB_SIZE_X_SHIFT) << x_shift;
|
||||
offset += SwizzleTable((y & 7) * 64 + (x & 63));
|
||||
|
||||
linear[(layer * height + y) * row_words + word] = swizzled[src_word_offset + offset / 4];
|
||||
}
|
||||
@@ -107,36 +107,22 @@ void ComputePipeline::Configure() {
|
||||
const auto read_handle{[&](const auto& desc, u32 index) {
|
||||
ASSERT(((qmd.const_buffer_enable_mask >> desc.cbuf_index) & 1) != 0);
|
||||
const u32 index_offset{index << desc.size_shift};
|
||||
u32 offset{desc.cbuf_offset + index_offset};
|
||||
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) {
|
||||
if (desc.has_index_cbuf) {
|
||||
ASSERT(((qmd.const_buffer_enable_mask >> desc.index_cbuf_index) & 1) != 0);
|
||||
offset += gpu_memory->Read<u32>(cbufs[desc.index_cbuf_index].Address() +
|
||||
desc.index_cbuf_offset)
|
||||
<< desc.index_shift;
|
||||
}
|
||||
}
|
||||
const u32 offset{desc.cbuf_offset + index_offset};
|
||||
const GPUVAddr addr{cbufs[desc.cbuf_index].Address() + offset};
|
||||
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
|
||||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
|
||||
if (desc.has_secondary) {
|
||||
ASSERT(((qmd.const_buffer_enable_mask >> desc.secondary_cbuf_index) & 1) != 0);
|
||||
const u32 secondary_offset{desc.secondary_cbuf_offset};
|
||||
const u32 secondary_offset{desc.secondary_cbuf_offset + index_offset};
|
||||
const GPUVAddr separate_addr{cbufs[desc.secondary_cbuf_index].Address() +
|
||||
secondary_offset};
|
||||
const u32 lhs_raw{(gpu_memory->Read<u32>(addr) << desc.shift_left) & desc.mask};
|
||||
const u32 rhs_raw{(gpu_memory->Read<u32>(separate_addr)
|
||||
<< desc.secondary_shift_left) &
|
||||
desc.secondary_mask};
|
||||
const u32 lhs_raw{gpu_memory->Read<u32>(addr) << desc.shift_left};
|
||||
const u32 rhs_raw{gpu_memory->Read<u32>(separate_addr)
|
||||
<< desc.secondary_shift_left};
|
||||
return TexturePair(lhs_raw | rhs_raw, via_header_index);
|
||||
}
|
||||
}
|
||||
u32 raw{gpu_memory->Read<u32>(addr)};
|
||||
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
|
||||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
|
||||
raw &= desc.mask;
|
||||
}
|
||||
return TexturePair(raw, via_header_index);
|
||||
return TexturePair(gpu_memory->Read<u32>(addr), via_header_index);
|
||||
}};
|
||||
const auto add_image{[&](const auto& desc, bool blacklist) {
|
||||
for (u32 index = 0; index < desc.count; ++index) {
|
||||
|
||||
@@ -308,37 +308,23 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) {
|
||||
const auto read_handle{[&](const auto& desc, u32 index) {
|
||||
ASSERT(cbufs[desc.cbuf_index].enabled);
|
||||
const u32 index_offset{index << desc.size_shift};
|
||||
u32 offset{desc.cbuf_offset + index_offset};
|
||||
if constexpr (std::is_same_v<decltype(desc), const TextureDescriptor&>) {
|
||||
if (desc.has_index_cbuf) {
|
||||
ASSERT(cbufs[desc.index_cbuf_index].enabled);
|
||||
offset += gpu_memory->Read<u32>(cbufs[desc.index_cbuf_index].address +
|
||||
desc.index_cbuf_offset)
|
||||
<< desc.index_shift;
|
||||
}
|
||||
}
|
||||
const u32 offset{desc.cbuf_offset + index_offset};
|
||||
const GPUVAddr addr{cbufs[desc.cbuf_index].address + offset};
|
||||
if constexpr (std::is_same_v<decltype(desc), const TextureDescriptor&> ||
|
||||
std::is_same_v<decltype(desc), const TextureBufferDescriptor&>) {
|
||||
if (desc.has_secondary) {
|
||||
ASSERT(cbufs[desc.secondary_cbuf_index].enabled);
|
||||
const u32 second_offset{desc.secondary_cbuf_offset};
|
||||
const u32 second_offset{desc.secondary_cbuf_offset + index_offset};
|
||||
const GPUVAddr separate_addr{cbufs[desc.secondary_cbuf_index].address +
|
||||
second_offset};
|
||||
const u32 lhs_raw{(gpu_memory->Read<u32>(addr) << desc.shift_left) & desc.mask};
|
||||
const u32 rhs_raw{(gpu_memory->Read<u32>(separate_addr)
|
||||
<< desc.secondary_shift_left) &
|
||||
desc.secondary_mask};
|
||||
const u32 lhs_raw{gpu_memory->Read<u32>(addr) << desc.shift_left};
|
||||
const u32 rhs_raw{gpu_memory->Read<u32>(separate_addr)
|
||||
<< desc.secondary_shift_left};
|
||||
const u32 raw{lhs_raw | rhs_raw};
|
||||
return TexturePair(raw, via_header_index);
|
||||
}
|
||||
}
|
||||
u32 raw{gpu_memory->Read<u32>(addr)};
|
||||
if constexpr (std::is_same_v<decltype(desc), const TextureDescriptor&> ||
|
||||
std::is_same_v<decltype(desc), const TextureBufferDescriptor&>) {
|
||||
raw &= desc.mask;
|
||||
}
|
||||
return TexturePair(raw, via_header_index);
|
||||
return TexturePair(gpu_memory->Read<u32>(addr), via_header_index);
|
||||
}};
|
||||
const auto add_image{[&](const auto& desc, bool blacklist) LAMBDA_FORCEINLINE {
|
||||
for (u32 index = 0; index < desc.count; ++index) {
|
||||
|
||||
@@ -129,13 +129,7 @@ void FixedPipelineState::Refresh(Tegra::Engines::Maxwell3D& maxwell3d, DynamicFe
|
||||
topology.Assign(can_collapse_topology_class
|
||||
? TOPOLOGY_CLASS_REPRESENTATIVE_LUT[static_cast<size_t>(topology_)]
|
||||
: topology_);
|
||||
// Target-independent rasterization rasterizes (and depth tests) at the raster sample count
|
||||
if (regs.tir_mode == Maxwell::TIRMode::RasterNTargetM &&
|
||||
regs.anti_alias_raster != Maxwell::AntiAliasRaster::Mode1x1) {
|
||||
msaa_mode.Assign(static_cast<Tegra::Texture::MsaaMode>(regs.anti_alias_raster));
|
||||
} else {
|
||||
msaa_mode.Assign(regs.anti_alias_samples_mode);
|
||||
}
|
||||
msaa_mode.Assign(regs.anti_alias_samples_mode);
|
||||
attachment0_dual_source_blend.Assign(ComputeAttachment0DualSourceBlend(regs) ? 1 : 0);
|
||||
|
||||
raw2 = 0;
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#include "video_core/host_shaders/queries_prefix_scan_sum_comp_spv.h"
|
||||
#include "video_core/host_shaders/queries_prefix_scan_sum_nosubgroups_comp_spv.h"
|
||||
#include "video_core/host_shaders/resolve_conditional_render_comp_spv.h"
|
||||
#include "video_core/host_shaders/vulkan_block_linear_unswizzle_2d_buffer_comp_spv.h"
|
||||
#include "video_core/host_shaders/vulkan_quad_indexed_comp_spv.h"
|
||||
#include "video_core/host_shaders/vulkan_uint8_comp_spv.h"
|
||||
#include "video_core/host_shaders/block_linear_unswizzle_3d_bcn_comp_spv.h"
|
||||
@@ -32,7 +31,6 @@
|
||||
#include "video_core/renderer_vulkan/vk_update_descriptor.h"
|
||||
#include "video_core/texture_cache/accelerated_swizzle.h"
|
||||
#include "video_core/texture_cache/types.h"
|
||||
#include "video_core/texture_cache/util.h"
|
||||
#include "video_core/textures/decoders.h"
|
||||
#include "video_core/vulkan_common/vulkan_device.h"
|
||||
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
||||
@@ -642,143 +640,6 @@ struct alignas(16) BlockLinearUnswizzle3DPushConstants {
|
||||
};
|
||||
static_assert(sizeof(BlockLinearUnswizzle3DPushConstants) <= 128);
|
||||
|
||||
struct BlockLinearUnswizzle2DBufferPushConstants {
|
||||
u32 src_word_offset;
|
||||
u32 row_words;
|
||||
u32 height;
|
||||
u32 layer_stride;
|
||||
u32 block_size;
|
||||
u32 x_shift;
|
||||
u32 block_height;
|
||||
u32 block_height_mask;
|
||||
};
|
||||
|
||||
BlockLinearUnswizzle2DBufferPass::BlockLinearUnswizzle2DBufferPass(
|
||||
const Device& device_, Scheduler& scheduler_, DescriptorPool& descriptor_pool_,
|
||||
StagingBufferPool& staging_buffer_pool_,
|
||||
ComputePassDescriptorQueue& compute_pass_descriptor_queue_)
|
||||
: ComputePass(device_, scheduler_, descriptor_pool_, INPUT_OUTPUT_DESCRIPTOR_SET_BINDINGS,
|
||||
INPUT_OUTPUT_DESCRIPTOR_UPDATE_TEMPLATE, INPUT_OUTPUT_BANK_INFO,
|
||||
COMPUTE_PUSH_CONSTANT_RANGE<sizeof(BlockLinearUnswizzle2DBufferPushConstants)>,
|
||||
VULKAN_BLOCK_LINEAR_UNSWIZZLE_2D_BUFFER_COMP_SPV),
|
||||
scheduler{scheduler_}, staging_buffer_pool{staging_buffer_pool_},
|
||||
compute_pass_descriptor_queue{compute_pass_descriptor_queue_} {}
|
||||
|
||||
BlockLinearUnswizzle2DBufferPass::~BlockLinearUnswizzle2DBufferPass() = default;
|
||||
|
||||
bool BlockLinearUnswizzle2DBufferPass::IsSupported(const Image& image) {
|
||||
const VideoCommon::ImageInfo& info = image.info;
|
||||
if (info.type != VideoCommon::ImageType::e2D || info.num_samples != 1 ||
|
||||
VideoCore::Surface::GetFormatType(info.format) != VideoCore::Surface::SurfaceType::ColorTexture ||
|
||||
VideoCore::Surface::DefaultBlockWidth(info.format) != 1 ||
|
||||
VideoCore::Surface::DefaultBlockHeight(info.format) != 1 ||
|
||||
True(image.flags & VideoCommon::ImageFlagBits::Converted) ||
|
||||
True(image.flags & VideoCommon::ImageFlagBits::Rescaled)) {
|
||||
return false;
|
||||
}
|
||||
const u32 bytes_per_block = VideoCore::Surface::BytesPerBlock(info.format);
|
||||
for (const VideoCommon::SwizzleParameters& swizzle : VideoCommon::FullUploadSwizzles(info)) {
|
||||
if ((swizzle.num_tiles.width * bytes_per_block) % 4 != 0 || swizzle.num_tiles.depth != 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::pair<VkBuffer, boost::container::small_vector<VkBufferImageCopy, 16>>
|
||||
BlockLinearUnswizzle2DBufferPass::Unswizzle(Image& image, VkBuffer src_buffer,
|
||||
VkDeviceSize src_offset) {
|
||||
const VideoCommon::ImageInfo& info = image.info;
|
||||
const auto swizzles = VideoCommon::FullUploadSwizzles(info);
|
||||
const u32 bytes_per_block = VideoCore::Surface::BytesPerBlock(info.format);
|
||||
const u32 layers = static_cast<u32>(info.resources.layers);
|
||||
const VkDeviceSize alignment = (std::max<VkDeviceSize>)(device.GetStorageBufferAlignment(), 4);
|
||||
|
||||
VkDeviceSize total_size = 0;
|
||||
for (const VideoCommon::SwizzleParameters& swizzle : swizzles) {
|
||||
total_size = Common::AlignUp(total_size, alignment);
|
||||
total_size += VkDeviceSize{swizzle.num_tiles.width} * bytes_per_block *
|
||||
swizzle.num_tiles.height * layers;
|
||||
}
|
||||
const StagingBufferRef linear = staging_buffer_pool.Request(total_size, MemoryUsage::DeviceLocal);
|
||||
|
||||
scheduler.RequestOutsideRenderPassOperationContext();
|
||||
scheduler.Record([](vk::CommandBuffer cmdbuf) {
|
||||
static constexpr VkMemoryBarrier READ_BARRIER{
|
||||
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
|
||||
.pNext = nullptr,
|
||||
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
|
||||
};
|
||||
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, READ_BARRIER);
|
||||
});
|
||||
|
||||
boost::container::small_vector<VkBufferImageCopy, 16> copies;
|
||||
VkDeviceSize linear_offset = 0;
|
||||
for (const VideoCommon::SwizzleParameters& swizzle : swizzles) {
|
||||
linear_offset = Common::AlignUp(linear_offset, alignment);
|
||||
const auto params = VideoCommon::Accelerated::MakeBlockLinearSwizzle2DParams(swizzle, info);
|
||||
const u32 row_words = swizzle.num_tiles.width * bytes_per_block / 4;
|
||||
const u32 height = swizzle.num_tiles.height;
|
||||
const VkDeviceSize level_size = VkDeviceSize{row_words} * 4 * height * layers;
|
||||
const VkDeviceSize level_src = src_offset + swizzle.buffer_offset;
|
||||
const VkDeviceSize aligned_src = level_src & ~(alignment - 1);
|
||||
const VkDeviceSize src_size = image.guest_size_bytes - swizzle.buffer_offset +
|
||||
(level_src - aligned_src);
|
||||
|
||||
compute_pass_descriptor_queue.Acquire(scheduler, 2);
|
||||
compute_pass_descriptor_queue.AddBuffer(src_buffer, aligned_src, src_size);
|
||||
compute_pass_descriptor_queue.AddBuffer(linear.buffer, linear.offset + linear_offset,
|
||||
level_size);
|
||||
const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()};
|
||||
const BlockLinearUnswizzle2DBufferPushConstants push{
|
||||
.src_word_offset = static_cast<u32>((level_src - aligned_src) / 4),
|
||||
.row_words = row_words,
|
||||
.height = height,
|
||||
.layer_stride = params.layer_stride,
|
||||
.block_size = params.block_size,
|
||||
.x_shift = params.x_shift,
|
||||
.block_height = params.block_height,
|
||||
.block_height_mask = params.block_height_mask,
|
||||
};
|
||||
scheduler.Record([this, descriptor_data, push, layers](vk::CommandBuffer cmdbuf) {
|
||||
static constexpr u32 WORKGROUP_SIZE_X = 64;
|
||||
const VkDescriptorSet set = descriptor_allocator.Commit();
|
||||
device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, descriptor_data);
|
||||
cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *pipeline);
|
||||
cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {});
|
||||
cmdbuf.PushConstants(*layout, VK_SHADER_STAGE_COMPUTE_BIT, push);
|
||||
cmdbuf.Dispatch(Common::DivCeil(push.row_words, WORKGROUP_SIZE_X), push.height, layers);
|
||||
});
|
||||
copies.push_back(VkBufferImageCopy{
|
||||
.bufferOffset = linear.offset + linear_offset,
|
||||
.bufferRowLength = 0,
|
||||
.bufferImageHeight = 0,
|
||||
.imageSubresource{
|
||||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
.mipLevel = static_cast<u32>(swizzle.level),
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = layers,
|
||||
},
|
||||
.imageOffset{0, 0, 0},
|
||||
.imageExtent{swizzle.num_tiles.width, height, 1},
|
||||
});
|
||||
linear_offset += level_size;
|
||||
}
|
||||
scheduler.Record([](vk::CommandBuffer cmdbuf) {
|
||||
static constexpr VkMemoryBarrier WRITE_BARRIER{
|
||||
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
|
||||
.pNext = nullptr,
|
||||
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
|
||||
.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT,
|
||||
};
|
||||
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, WRITE_BARRIER);
|
||||
});
|
||||
return {linear.buffer, std::move(copies)};
|
||||
}
|
||||
|
||||
BlockLinearUnswizzle3DPass::BlockLinearUnswizzle3DPass(
|
||||
const Device& device_, Scheduler& scheduler_,
|
||||
DescriptorPool& descriptor_pool_,
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
#include <span>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/container/small_vector.hpp>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "video_core/engines/maxwell_3d.h"
|
||||
#include "video_core/renderer_vulkan/vk_descriptor_pool.h"
|
||||
@@ -139,28 +137,6 @@ private:
|
||||
MemoryAllocator& memory_allocator;
|
||||
};
|
||||
|
||||
class BlockLinearUnswizzle2DBufferPass final : public ComputePass {
|
||||
public:
|
||||
explicit BlockLinearUnswizzle2DBufferPass(
|
||||
const Device& device_, Scheduler& scheduler_, DescriptorPool& descriptor_pool_,
|
||||
StagingBufferPool& staging_buffer_pool_,
|
||||
ComputePassDescriptorQueue& compute_pass_descriptor_queue_);
|
||||
~BlockLinearUnswizzle2DBufferPass();
|
||||
|
||||
/// Returns true when the guest layout of the image can be unswizzled by this pass
|
||||
[[nodiscard]] static bool IsSupported(const Image& image);
|
||||
|
||||
/// Unswizzles the block linear guest data of the image, read from a GPU buffer, into a linear
|
||||
/// buffer. Returns the linear buffer and the copies that upload it into the image.
|
||||
std::pair<VkBuffer, boost::container::small_vector<VkBufferImageCopy, 16>> Unswizzle(
|
||||
Image& image, VkBuffer src_buffer, VkDeviceSize src_offset);
|
||||
|
||||
private:
|
||||
Scheduler& scheduler;
|
||||
StagingBufferPool& staging_buffer_pool;
|
||||
ComputePassDescriptorQueue& compute_pass_descriptor_queue;
|
||||
};
|
||||
|
||||
class BlockLinearUnswizzle3DPass final : public ComputePass {
|
||||
public:
|
||||
explicit BlockLinearUnswizzle3DPass(const Device& device_, Scheduler& scheduler_,
|
||||
|
||||
@@ -174,36 +174,21 @@ bool ComputePipeline::Configure(Tegra::Engines::KeplerCompute& kepler_compute,
|
||||
const auto read_handle{[&](const auto& desc, u32 index) {
|
||||
ASSERT(((qmd.const_buffer_enable_mask >> desc.cbuf_index) & 1) != 0);
|
||||
const u32 index_offset{index << desc.size_shift};
|
||||
u32 offset{desc.cbuf_offset + index_offset};
|
||||
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) {
|
||||
if (desc.has_index_cbuf) {
|
||||
ASSERT(((qmd.const_buffer_enable_mask >> desc.index_cbuf_index) & 1) != 0);
|
||||
offset += gpu_memory.Read<u32>(cbufs[desc.index_cbuf_index].Address() +
|
||||
desc.index_cbuf_offset)
|
||||
<< desc.index_shift;
|
||||
}
|
||||
}
|
||||
const u32 offset{desc.cbuf_offset + index_offset};
|
||||
const GPUVAddr addr{cbufs[desc.cbuf_index].Address() + offset};
|
||||
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
|
||||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
|
||||
if (desc.has_secondary) {
|
||||
ASSERT(((qmd.const_buffer_enable_mask >> desc.secondary_cbuf_index) & 1) != 0);
|
||||
const u32 secondary_offset{desc.secondary_cbuf_offset};
|
||||
const u32 secondary_offset{desc.secondary_cbuf_offset + index_offset};
|
||||
const GPUVAddr separate_addr{cbufs[desc.secondary_cbuf_index].Address() +
|
||||
secondary_offset};
|
||||
const u32 lhs_raw{(gpu_memory.Read<u32>(addr) << desc.shift_left) & desc.mask};
|
||||
const u32 rhs_raw{(gpu_memory.Read<u32>(separate_addr)
|
||||
<< desc.secondary_shift_left) &
|
||||
desc.secondary_mask};
|
||||
const u32 lhs_raw{gpu_memory.Read<u32>(addr) << desc.shift_left};
|
||||
const u32 rhs_raw{gpu_memory.Read<u32>(separate_addr) << desc.secondary_shift_left};
|
||||
return TexturePair(lhs_raw | rhs_raw, via_header_index);
|
||||
}
|
||||
}
|
||||
u32 raw{gpu_memory.Read<u32>(addr)};
|
||||
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
|
||||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
|
||||
raw &= desc.mask;
|
||||
}
|
||||
return TexturePair(raw, via_header_index);
|
||||
return TexturePair(gpu_memory.Read<u32>(addr), via_header_index);
|
||||
}};
|
||||
const auto add_image{[&](const auto& desc, bool blacklist) {
|
||||
for (u32 index = 0; index < desc.count; ++index) {
|
||||
|
||||
@@ -377,37 +377,23 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) {
|
||||
const auto read_handle{[&](const auto& desc, u32 index) {
|
||||
ASSERT(cbufs[desc.cbuf_index].enabled);
|
||||
const u32 index_offset{index << desc.size_shift};
|
||||
u32 offset{desc.cbuf_offset + index_offset};
|
||||
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) {
|
||||
if (desc.has_index_cbuf) {
|
||||
ASSERT(cbufs[desc.index_cbuf_index].enabled);
|
||||
offset += gpu_memory->Read<u32>(cbufs[desc.index_cbuf_index].address +
|
||||
desc.index_cbuf_offset)
|
||||
<< desc.index_shift;
|
||||
}
|
||||
}
|
||||
const u32 offset{desc.cbuf_offset + index_offset};
|
||||
const GPUVAddr addr{cbufs[desc.cbuf_index].address + offset};
|
||||
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
|
||||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
|
||||
if (desc.has_secondary) {
|
||||
ASSERT(cbufs[desc.secondary_cbuf_index].enabled);
|
||||
const u32 second_offset{desc.secondary_cbuf_offset};
|
||||
const u32 second_offset{desc.secondary_cbuf_offset + index_offset};
|
||||
const GPUVAddr separate_addr{cbufs[desc.secondary_cbuf_index].address +
|
||||
second_offset};
|
||||
const u32 lhs_raw{(gpu_memory->Read<u32>(addr) << desc.shift_left) & desc.mask};
|
||||
const u32 rhs_raw{(gpu_memory->Read<u32>(separate_addr)
|
||||
<< desc.secondary_shift_left) &
|
||||
desc.secondary_mask};
|
||||
const u32 lhs_raw{gpu_memory->Read<u32>(addr) << desc.shift_left};
|
||||
const u32 rhs_raw{gpu_memory->Read<u32>(separate_addr)
|
||||
<< desc.secondary_shift_left};
|
||||
const u32 raw{lhs_raw | rhs_raw};
|
||||
return TexturePair(raw, via_header_index);
|
||||
}
|
||||
}
|
||||
u32 raw{gpu_memory->Read<u32>(addr)};
|
||||
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
|
||||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
|
||||
raw &= desc.mask;
|
||||
}
|
||||
return TexturePair(raw, via_header_index);
|
||||
return TexturePair(gpu_memory->Read<u32>(addr), via_header_index);
|
||||
}};
|
||||
const auto add_image{[&](const auto& desc, bool blacklist) LAMBDA_FORCEINLINE {
|
||||
for (u32 index = 0; index < desc.count; ++index) {
|
||||
|
||||
@@ -596,12 +596,6 @@ void RasterizerVulkan::DispatchCompute() {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto update_images_written_by_buffers = [this] {
|
||||
buffer_cache.ForEachWrittenComputeStorageBuffer(
|
||||
[this](auto& buffer, u32 offset, DAddr device_addr, u32 size) {
|
||||
texture_cache.UpdateImagesFromBuffer(device_addr, size, buffer.Handle(), offset);
|
||||
});
|
||||
};
|
||||
const auto& qmd{kepler_compute->launch_description};
|
||||
auto indirect_address = kepler_compute->GetIndirectComputeAddress();
|
||||
if (indirect_address) {
|
||||
@@ -618,7 +612,6 @@ void RasterizerVulkan::DispatchCompute() {
|
||||
}
|
||||
cmdbuf.DispatchIndirect(indirect_buffer, indirect_offset);
|
||||
});
|
||||
update_images_written_by_buffers();
|
||||
return;
|
||||
}
|
||||
const std::array<u32, 3> dim{qmd.grid_dim_x, qmd.grid_dim_y, qmd.grid_dim_z};
|
||||
@@ -641,7 +634,6 @@ void RasterizerVulkan::DispatchCompute() {
|
||||
}
|
||||
cmdbuf.Dispatch(dim[0], dim[1], dim[2]);
|
||||
});
|
||||
update_images_written_by_buffers();
|
||||
|
||||
// Log compute dispatch
|
||||
if (GPU::Logging::IsActive() &&
|
||||
|
||||
@@ -946,8 +946,6 @@ TextureCacheRuntime::TextureCacheRuntime(const Device& device_, Scheduler& sched
|
||||
astc_decoder_pass.emplace(device, scheduler, descriptor_pool, staging_buffer_pool,
|
||||
compute_pass_descriptor_queue, memory_allocator);
|
||||
}
|
||||
bl2d_buffer_unswizzle_pass.emplace(device, scheduler, descriptor_pool, staging_buffer_pool,
|
||||
compute_pass_descriptor_queue);
|
||||
if (!device.IsKhrImageFormatListSupported()) {
|
||||
return;
|
||||
}
|
||||
@@ -1091,58 +1089,6 @@ VkImageView TextureCacheRuntime::GetOrCreateResolveShadow(VkImage msaa_image, Vk
|
||||
return *shadow.view;
|
||||
}
|
||||
|
||||
VkImageView TextureCacheRuntime::GetOrCreateTirColorScratch(VkImage color_image, VkFormat format,
|
||||
VkExtent2D extent, u32 layers,
|
||||
VkSampleCountFlagBits samples) {
|
||||
TirColorScratch& scratch = tir_color_scratch[color_image];
|
||||
if (scratch.image && scratch.format == format && scratch.extent.width == extent.width &&
|
||||
scratch.extent.height == extent.height && scratch.layers == layers &&
|
||||
scratch.samples == samples) {
|
||||
return *scratch.view;
|
||||
}
|
||||
if (scratch.image) {
|
||||
scheduler.Finish();
|
||||
}
|
||||
scratch.image = memory_allocator.CreateImage(VkImageCreateInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
.imageType = VK_IMAGE_TYPE_2D,
|
||||
.format = format,
|
||||
.extent = {extent.width, extent.height, 1},
|
||||
.mipLevels = 1,
|
||||
.arrayLayers = layers,
|
||||
.samples = samples,
|
||||
.tiling = VK_IMAGE_TILING_OPTIMAL,
|
||||
.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
|
||||
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
|
||||
.queueFamilyIndexCount = 0,
|
||||
.pQueueFamilyIndices = nullptr,
|
||||
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
});
|
||||
scratch.view = device.GetLogical().CreateImageView(VkImageViewCreateInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
.image = *scratch.image,
|
||||
.viewType = layers > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D,
|
||||
.format = format,
|
||||
.components{},
|
||||
.subresourceRange{
|
||||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
.baseMipLevel = 0,
|
||||
.levelCount = 1,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = layers,
|
||||
},
|
||||
});
|
||||
scratch.format = format;
|
||||
scratch.extent = extent;
|
||||
scratch.layers = layers;
|
||||
scratch.samples = samples;
|
||||
return *scratch.view;
|
||||
}
|
||||
|
||||
const TextureCacheRuntime::ResolveShadow* TextureCacheRuntime::GetValidResolveShadow(
|
||||
VkImage msaa_image) const {
|
||||
const auto it = resolve_shadows.find(msaa_image);
|
||||
@@ -2122,27 +2068,6 @@ void Image::UploadMemory(VkBuffer buffer, VkDeviceSize offset,
|
||||
}
|
||||
}
|
||||
|
||||
bool Image::UploadSwizzledFromBuffer(VkBuffer buffer, VkDeviceSize offset) {
|
||||
if (runtime == nullptr || !runtime->bl2d_buffer_unswizzle_pass || offset % 4 != 0 ||
|
||||
guest_size_bytes > runtime->device.GetMaxStorageBufferRange() ||
|
||||
!BlockLinearUnswizzle2DBufferPass::IsSupported(*this)) {
|
||||
return false;
|
||||
}
|
||||
if (ENABLE_MSAA_RESOLVE_CONSUME) {
|
||||
runtime->InvalidateResolveShadow(Handle());
|
||||
}
|
||||
auto [linear_buffer, copies] =
|
||||
runtime->bl2d_buffer_unswizzle_pass->Unswizzle(*this, buffer, offset);
|
||||
const VkImage vk_image = *original_image;
|
||||
const VkImageAspectFlags vk_aspect_mask = aspect_mask;
|
||||
const bool was_initialized = std::exchange(initialized, true);
|
||||
scheduler->Record([linear_buffer, vk_image, vk_aspect_mask, was_initialized,
|
||||
copies = std::move(copies)](vk::CommandBuffer cmdbuf) {
|
||||
CopyBufferToImage(cmdbuf, linear_buffer, vk_image, vk_aspect_mask, was_initialized, copies);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void Image::UploadMemory(const StagingBufferRef& map, std::span<const BufferImageCopy> copies) {
|
||||
UploadMemory(map.buffer, map.offset, copies);
|
||||
}
|
||||
@@ -3045,26 +2970,6 @@ Framebuffer::Framebuffer(TextureCacheRuntime& runtime, ImageView* color_buffer,
|
||||
|
||||
Framebuffer::~Framebuffer() = default;
|
||||
|
||||
/// Single-sampled color targets with a multisampled depth buffer, as used by target-independent
|
||||
/// rasterization. Vulkan requires matching sample counts for these attachments.
|
||||
static bool IsMixedSamplesTarget(std::span<ImageView*, NUM_RT> color_buffers,
|
||||
const ImageView* depth_buffer) {
|
||||
if (depth_buffer == nullptr || depth_buffer->Samples() == VK_SAMPLE_COUNT_1_BIT) {
|
||||
return false;
|
||||
}
|
||||
bool has_color = false;
|
||||
for (const ImageView* const color_buffer : color_buffers) {
|
||||
if (color_buffer == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if (color_buffer->Samples() != VK_SAMPLE_COUNT_1_BIT) {
|
||||
return false;
|
||||
}
|
||||
has_color = true;
|
||||
}
|
||||
return has_color;
|
||||
}
|
||||
|
||||
void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime,
|
||||
std::span<ImageView*, NUM_RT> color_buffers,
|
||||
ImageView* depth_buffer, bool is_rescaled_) {
|
||||
@@ -3119,13 +3024,6 @@ void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime,
|
||||
} else {
|
||||
renderpass_key.depth_format = PixelFormat::Invalid;
|
||||
}
|
||||
if (IsMixedSamplesTarget(color_buffers, depth_buffer)) {
|
||||
const VkExtent2D extent{(std::min)(render_area.width, width),
|
||||
(std::min)(render_area.height, height)};
|
||||
CreateMixedSamplesFramebuffer(runtime, color_buffers, attachments, renderpass_key,
|
||||
static_cast<u32>((std::max)(num_layers, 1)), extent);
|
||||
return;
|
||||
}
|
||||
renderpass_key.samples = samples;
|
||||
const bool do_resolve_color = ENABLE_MSAA_TILER_RESOLVE &&
|
||||
samples != VK_SAMPLE_COUNT_1_BIT && num_colors > 0 && runtime.device.IsTiler();
|
||||
@@ -3142,7 +3040,10 @@ void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime,
|
||||
discard_msaa_depth_stencil = ENABLE_MSAA_RESOLVE_CONSUME &&
|
||||
ENABLE_MSAA_DEPTH_STENCIL_DISCARD && do_resolve_depth_stencil;
|
||||
|
||||
UseRenderPass(runtime, renderpass_key);
|
||||
renderpass = runtime.render_pass_cache.Get(renderpass_key);
|
||||
render_pass_key = renderpass_key;
|
||||
render_pass_cache = &runtime.render_pass_cache;
|
||||
runtime_ptr = &runtime;
|
||||
render_area.width = (std::min)(render_area.width, width);
|
||||
render_area.height = (std::min)(render_area.height, height);
|
||||
|
||||
@@ -3174,53 +3075,6 @@ void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime,
|
||||
}
|
||||
|
||||
num_color_buffers = static_cast<u32>(num_colors);
|
||||
CreateVkFramebuffer(runtime, attachments, static_cast<u32>((std::max)(num_layers, 1)));
|
||||
}
|
||||
|
||||
void Framebuffer::CreateMixedSamplesFramebuffer(TextureCacheRuntime& runtime,
|
||||
std::span<ImageView*, NUM_RT> color_buffers,
|
||||
std::span<const VkImageView> attachments,
|
||||
RenderPassKey renderpass_key, u32 layers,
|
||||
VkExtent2D extent) {
|
||||
// attachments holds the real color targets followed by the depth buffer
|
||||
const size_t num_colors = attachments.size() - 1;
|
||||
boost::container::small_vector<VkImageView, NUM_RT * 2 + 2> mixed_attachments;
|
||||
for (const ImageView* const color_buffer : color_buffers) {
|
||||
if (!color_buffer) {
|
||||
continue;
|
||||
}
|
||||
const VkFormat vk_format = MaxwellToVK::SurfaceFormat(runtime.device, FormatType::Optimal,
|
||||
true, color_buffer->format)
|
||||
.format;
|
||||
mixed_attachments.push_back(runtime.GetOrCreateTirColorScratch(
|
||||
color_buffer->ImageHandle(), vk_format, extent, layers, samples));
|
||||
}
|
||||
mixed_attachments.push_back(attachments[num_colors]);
|
||||
for (size_t index = 0; index < num_colors; ++index) {
|
||||
mixed_attachments.push_back(attachments[index]);
|
||||
}
|
||||
// Multisampled scratch color + depth, resolving color into the real single-sampled targets
|
||||
renderpass_key.samples = samples;
|
||||
renderpass_key.resolve_color = true;
|
||||
renderpass_key.resolve_depth_stencil = false;
|
||||
discard_msaa_color = false;
|
||||
discard_msaa_depth_stencil = false;
|
||||
UseRenderPass(runtime, renderpass_key);
|
||||
render_area = extent;
|
||||
num_color_buffers = static_cast<u32>(num_colors);
|
||||
CreateVkFramebuffer(runtime, mixed_attachments, layers);
|
||||
}
|
||||
|
||||
void Framebuffer::UseRenderPass(TextureCacheRuntime& runtime,
|
||||
const RenderPassKey& renderpass_key) {
|
||||
renderpass = runtime.render_pass_cache.Get(renderpass_key);
|
||||
render_pass_key = renderpass_key;
|
||||
render_pass_cache = &runtime.render_pass_cache;
|
||||
runtime_ptr = &runtime;
|
||||
}
|
||||
|
||||
void Framebuffer::CreateVkFramebuffer(TextureCacheRuntime& runtime,
|
||||
std::span<const VkImageView> attachments, u32 layers) {
|
||||
framebuffer = runtime.device.GetLogical().CreateFramebuffer({
|
||||
.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
@@ -3230,7 +3084,7 @@ void Framebuffer::CreateVkFramebuffer(TextureCacheRuntime& runtime,
|
||||
.pAttachments = attachments.data(),
|
||||
.width = render_area.width,
|
||||
.height = render_area.height,
|
||||
.layers = layers,
|
||||
.layers = static_cast<u32>((std::max)(num_layers, 1)),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -133,11 +133,6 @@ public:
|
||||
|
||||
[[nodiscard]] const ResolveShadow* GetValidResolveShadow(VkImage msaa_image) const;
|
||||
|
||||
/// Multisampled stand-in for a single-sampled color target rendered with a TIR depth buffer
|
||||
[[nodiscard]] VkImageView GetOrCreateTirColorScratch(VkImage color_image, VkFormat format,
|
||||
VkExtent2D extent, u32 layers,
|
||||
VkSampleCountFlagBits samples);
|
||||
|
||||
void InvalidateResolveShadow(VkImage msaa_image);
|
||||
|
||||
void MarkResolveShadowUpToDate(VkImage msaa_image);
|
||||
@@ -164,7 +159,6 @@ public:
|
||||
std::optional<ASTCDecoderPass> astc_decoder_pass;
|
||||
|
||||
std::optional<BlockLinearUnswizzle3DPass> bl3d_unswizzle_pass;
|
||||
std::optional<BlockLinearUnswizzle2DBufferPass> bl2d_buffer_unswizzle_pass;
|
||||
const Settings::ResolutionScalingInfo& resolution;
|
||||
std::array<std::vector<VkFormat>, VideoCore::Surface::MaxPixelFormat> view_formats;
|
||||
|
||||
@@ -193,15 +187,6 @@ public:
|
||||
|
||||
std::vector<MsaaScratchImage> msaa_scratch_images;
|
||||
::Common::unordered_map<VkImage, ResolveShadow> resolve_shadows;
|
||||
struct TirColorScratch {
|
||||
vk::Image image;
|
||||
vk::ImageView view;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkExtent2D extent{};
|
||||
u32 layers = 0;
|
||||
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
};
|
||||
::Common::unordered_map<VkImage, TirColorScratch> tir_color_scratch;
|
||||
std::vector<std::pair<u64, ResolveShadow>> pending_resolve_shadows;
|
||||
};
|
||||
|
||||
@@ -294,22 +279,6 @@ public:
|
||||
void MarkResolveShadowsUpToDate() const;
|
||||
|
||||
private:
|
||||
/// Builds a framebuffer for single-sampled color targets drawn with a multisampled depth
|
||||
/// buffer (target-independent rasterization). Color is rendered into multisampled scratch
|
||||
/// images that resolve into the real targets at the end of the pass.
|
||||
void CreateMixedSamplesFramebuffer(TextureCacheRuntime& runtime,
|
||||
std::span<ImageView*, NUM_RT> color_buffers,
|
||||
std::span<const VkImageView> attachments,
|
||||
RenderPassKey renderpass_key, u32 layers,
|
||||
VkExtent2D extent);
|
||||
|
||||
/// Fetches the render pass for the key from the cache and remembers it for variants.
|
||||
void UseRenderPass(TextureCacheRuntime& runtime, const RenderPassKey& renderpass_key);
|
||||
|
||||
/// Creates the Vulkan framebuffer for the current render pass and render area.
|
||||
void CreateVkFramebuffer(TextureCacheRuntime& runtime,
|
||||
std::span<const VkImageView> attachments, u32 layers);
|
||||
|
||||
static constexpr size_t NUM_MEMOIZED_RENDER_PASS_VARIANTS = 8;
|
||||
|
||||
vk::Framebuffer framebuffer;
|
||||
@@ -356,10 +325,6 @@ public:
|
||||
void UploadMemory(const StagingBufferRef& map,
|
||||
std::span<const VideoCommon::BufferImageCopy> copies);
|
||||
|
||||
/// Replaces the image contents with its block linear guest data held in a GPU buffer.
|
||||
/// Returns false when the image layout is not supported.
|
||||
bool UploadSwizzledFromBuffer(VkBuffer buffer, VkDeviceSize offset);
|
||||
|
||||
void DownloadMemory(VkBuffer buffer, size_t offset,
|
||||
std::span<const VideoCommon::BufferImageCopy> copies);
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -9,6 +6,4 @@
|
||||
|
||||
namespace VideoCommon {
|
||||
template class VideoCommon::TextureCache<Vulkan::TextureCacheParams>;
|
||||
template void VideoCommon::TextureCache<Vulkan::TextureCacheParams>::UpdateImagesFromBuffer<VkBuffer>(
|
||||
DAddr, size_t, VkBuffer, u64);
|
||||
}
|
||||
|
||||
@@ -595,36 +595,6 @@ void TextureCache<P>::WriteMemory(DAddr cpu_addr, size_t size) {
|
||||
});
|
||||
}
|
||||
|
||||
template <class P>
|
||||
template <typename BufferHandle>
|
||||
void TextureCache<P>::UpdateImagesFromBuffer(DAddr cpu_addr, size_t size, BufferHandle buffer,
|
||||
u64 buffer_offset) {
|
||||
if constexpr (requires(Image& image) { image.UploadSwizzledFromBuffer(buffer, buffer_offset); }) {
|
||||
// The guest can write a render target through a storage buffer bound over its memory
|
||||
// (e.g. a compute clear). Only images the binding matches exactly are replaced, a larger
|
||||
// binding may hold unrelated live images in the parts the shader does not write.
|
||||
static constexpr size_t MAX_BINDING_PADDING = 64 * 1024;
|
||||
ForEachImageInRegion(cpu_addr, size, [&](ImageId image_id, Image& image) {
|
||||
if (image.cpu_addr != cpu_addr || size < image.guest_size_bytes ||
|
||||
size - image.guest_size_bytes > MAX_BINDING_PADDING ||
|
||||
True(image.flags & ImageFlagBits::Remapped) ||
|
||||
True(image.flags & ImageFlagBits::Sparse)) {
|
||||
return;
|
||||
}
|
||||
if (!image.UploadSwizzledFromBuffer(buffer, buffer_offset)) {
|
||||
return;
|
||||
}
|
||||
if (True(image.flags & ImageFlagBits::CpuModified)) {
|
||||
image.flags &= ~ImageFlagBits::CpuModified;
|
||||
if (False(image.flags & ImageFlagBits::Tracked)) {
|
||||
TrackImage(image, image_id);
|
||||
}
|
||||
}
|
||||
MarkModification(image);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
template <class P>
|
||||
bool TextureCache<P>::IsDownloadable(const ImageBase& image) const noexcept {
|
||||
if (!image.IsSafeGpuCopy()) {
|
||||
@@ -1958,26 +1928,7 @@ ImageViewId TextureCache<P>::FindDepthBuffer() {
|
||||
if (gpu_addr == 0) {
|
||||
return ImageViewId{};
|
||||
}
|
||||
// Target-independent rasterization: the depth buffer keeps the raster sample count while
|
||||
// the color targets use anti_alias_samples_mode.
|
||||
Tegra::Texture::MsaaMode zeta_msaa_mode = regs.anti_alias_samples_mode;
|
||||
if (regs.anti_alias_raster != Tegra::Engines::Maxwell3D::Regs::AntiAliasRaster::Mode1x1) {
|
||||
const auto raster_mode = static_cast<Tegra::Texture::MsaaMode>(regs.anti_alias_raster);
|
||||
bool use_raster = regs.tir_mode == Tegra::Engines::Maxwell3D::Regs::TIRMode::RasterNTargetM;
|
||||
if (!use_raster && regs.anti_alias_samples_mode == Tegra::Texture::MsaaMode::Msaa1x1) {
|
||||
// Clears can be issued before tir_mode is enabled. Only a depth buffer sized for the
|
||||
// raster samples relative to the color target is treated as multisampled, otherwise
|
||||
// it ping-pongs between a 1x and a multisampled image every frame.
|
||||
const auto& rt0 = regs.rt[0];
|
||||
use_raster = rt0.Address() != 0 && rt0.format != Tegra::RenderTargetFormat::NONE &&
|
||||
regs.zeta_size.width == rt0.width * NumSamplesX(raster_mode) &&
|
||||
regs.zeta_size.height == rt0.height * NumSamplesY(raster_mode);
|
||||
}
|
||||
if (use_raster) {
|
||||
zeta_msaa_mode = raster_mode;
|
||||
}
|
||||
}
|
||||
const ImageInfo info(regs.zeta, regs.zeta_size, zeta_msaa_mode);
|
||||
const ImageInfo info(regs.zeta, regs.zeta_size, regs.anti_alias_samples_mode);
|
||||
return FindRenderTargetView(info, gpu_addr);
|
||||
}
|
||||
|
||||
|
||||
@@ -203,12 +203,6 @@ public:
|
||||
/// Mark images in a range as modified from the CPU
|
||||
void WriteMemory(DAddr cpu_addr, size_t size);
|
||||
|
||||
/// Replaces the contents of the images fully inside a range that a GPU buffer wrote, with the
|
||||
/// data of that buffer
|
||||
template <typename BufferHandle>
|
||||
void UpdateImagesFromBuffer(DAddr cpu_addr, size_t size, BufferHandle buffer,
|
||||
u64 buffer_offset);
|
||||
|
||||
/// Download contents of host images to guest memory in a region
|
||||
void DownloadMemory(DAddr cpu_addr, size_t size);
|
||||
|
||||
|
||||
@@ -2797,9 +2797,9 @@ void MainWindow::IncrementInstallProgress() {
|
||||
|
||||
void MainWindow::OnMenuInstallToNAND() {
|
||||
const QString file_filter =
|
||||
tr("Installable Switch File (*.nca *.nsp *.xci);;Nintendo Content Archive "
|
||||
"(*.nca);;Nintendo Submission Package (*.nsp);;NX Cartridge "
|
||||
"Image (*.xci)");
|
||||
tr("Installable Switch File (*.nca *.nsp *.nsz *.xci *.xcz);;Nintendo Content Archive "
|
||||
"(*.nca);;Nintendo Submission Package (*.nsp *.nsz);;NX Cartridge "
|
||||
"Image (*.xci *.xcz)");
|
||||
|
||||
QStringList filenames = QFileDialog::getOpenFileNames(
|
||||
this, tr("Install Files"), QString::fromStdString(UISettings::values.roms_path),
|
||||
@@ -2856,7 +2856,10 @@ void MainWindow::OnMenuInstallToNAND() {
|
||||
QFuture<ContentManager::InstallResult> future;
|
||||
ContentManager::InstallResult result;
|
||||
|
||||
if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
|
||||
if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive) ||
|
||||
file.endsWith(QStringLiteral("nsz"), Qt::CaseInsensitive) ||
|
||||
file.endsWith(QStringLiteral("xci"), Qt::CaseInsensitive) ||
|
||||
file.endsWith(QStringLiteral("xcz"), Qt::CaseInsensitive)) {
|
||||
const auto progress_callback = [this](size_t size, size_t progress) {
|
||||
emit UpdateInstallProgress();
|
||||
if (install_progress->wasCanceled()) {
|
||||
|
||||
Reference in New Issue
Block a user