#include "Runtime/CDvdFile.hpp" // #include #include #include #include #include #include #include "Runtime/CDvdRequest.hpp" #include "Runtime/Logging.hpp" #include "Runtime/CStopwatch.hpp" namespace metaforce { namespace { struct SDLDiscStreamCtx { SDL_IOStream* io = nullptr; }; int64_t sdlStreamReadAt(void* userData, uint64_t offset, void* out, size_t len) { auto* ctx = static_cast(userData); if (ctx == nullptr || ctx->io == nullptr || offset > uint64_t(std::numeric_limits::max())) { return -1; } if (SDL_SeekIO(ctx->io, static_cast(offset), SDL_IO_SEEK_SET) < 0) { return -1; } size_t total = 0; auto* dst = static_cast(out); while (total < len) { const size_t read = SDL_ReadIO(ctx->io, dst + total, len - total); if (read == 0) { break; } total += read; } return static_cast(total); } int64_t sdlStreamLen(void* userData) { auto* ctx = static_cast(userData); if (ctx == nullptr || ctx->io == nullptr) { return -1; } const Sint64 size = SDL_GetIOSize(ctx->io); return size < 0 ? -1 : static_cast(size); } void sdlStreamClose(void* userData) { auto* ctx = static_cast(userData); if (ctx == nullptr) { return; } if (ctx->io != nullptr) { SDL_CloseIO(ctx->io); } delete ctx; } u32 nodReadLoop(NodHandle* reader, void* buf, u32 len) { if (reader == nullptr || buf == nullptr || len == 0) { return 0; } auto* out = static_cast(buf); u32 totalRead = 0; while (totalRead < len) { const u32 remaining = len - totalRead; const int64_t read = nod_read(reader, out + totalRead, remaining); if (read <= 0) { break; } if (read > int64_t(remaining)) { totalRead = len; break; } totalRead += u32(read); } return totalRead; } } // namespace CDvdFile::NodHandleUnique CDvdFile::m_DvdRoot{nullptr, nod_free}; CDvdFile::NodHandleUnique CDvdFile::m_DataPartition{nullptr, nod_free}; std::unordered_map CDvdFile::m_FileEntries; class CFileDvdRequest : public IDvdRequest { std::shared_ptr m_reader; uint64_t m_begin; uint64_t m_size; void* m_buf; u32 m_len; ESeekOrigin m_whence; int m_offset; bool m_cancel = false; bool m_complete = false; std::function m_callback; public: ~CFileDvdRequest() override { CFileDvdRequest::PostCancelRequest(); } void WaitUntilComplete() override { if (!m_complete && !m_cancel) { CDvdFile::DoWork(); } } bool IsComplete() override { if (!m_complete) { CDvdFile::DoWork(); } return m_complete; } void PostCancelRequest() override { m_cancel = true; } [[nodiscard]] EMediaType GetMediaType() const override { return EMediaType::File; } CFileDvdRequest(CDvdFile& file, void* buf, u32 len, ESeekOrigin whence, int off, std::function&& cb) : m_reader(file.m_reader) , m_begin(file.m_begin) , m_size(file.m_size) , m_buf(buf) , m_len(len) , m_whence(whence) , m_offset(off) , m_callback(std::move(cb)) {} void DoRequest() { if (m_cancel) { return; } if (!m_reader) { m_complete = true; if (m_callback) { m_callback(0); } return; } u32 readLen = 0; if (m_whence == ESeekOrigin::Cur && m_offset == 0) { readLen = nodReadLoop(m_reader.get(), m_buf, m_len); } else { int seek = 0; int64_t offset = m_offset; switch (m_whence) { case ESeekOrigin::Begin: { seek = 0; offset += int64_t(m_begin); break; } case ESeekOrigin::End: { seek = 0; offset += int64_t(m_begin) + int64_t(m_size); break; } case ESeekOrigin::Cur: { seek = 1; break; } }; if (nod_seek(m_reader.get(), offset, seek) >= 0) { readLen = nodReadLoop(m_reader.get(), m_buf, m_len); } } if (m_callback) { m_callback(readLen); } m_complete = true; } }; std::vector> CDvdFile::m_RequestQueue; std::string CDvdFile::m_rootDirectory; std::string CDvdFile::m_lastError; std::unique_ptr CDvdFile::m_dolBuf; size_t CDvdFile::m_dolBufLen = 0; CDvdFile::CDvdFile(std::string_view path) : x18_path(path) { const SFileEntry* entry = ResolvePath(path); if (entry == nullptr) { return; } NodHandle* fileRaw = nullptr; if (nod_partition_open_file(m_DataPartition.get(), entry->fstIndex, &fileRaw) == NOD_RESULT_OK && fileRaw != nullptr) { m_reader = std::shared_ptr(fileRaw, nod_free); m_size = entry->size; } } // single-threaded hack void CDvdFile::DoWork() { for (std::shared_ptr& req : m_RequestQueue) { auto& concreteReq = static_cast(*req); concreteReq.DoRequest(); } m_RequestQueue.clear(); } std::shared_ptr CDvdFile::AsyncSeekRead(void* buf, u32 len, ESeekOrigin whence, int off, std::function&& cb) { std::shared_ptr ret = std::make_shared(*this, buf, len, whence, off, std::move(cb)); m_RequestQueue.emplace_back(ret); return ret; } u32 CDvdFile::SyncSeekRead(void* buf, u32 len, ESeekOrigin whence, int offset) { if (!m_reader) { return 0; } int seek = 0; int64_t seekOffset = offset; switch (whence) { case ESeekOrigin::Begin: { seek = 0; seekOffset += int64_t(m_begin); break; } case ESeekOrigin::End: { seek = 0; seekOffset += int64_t(m_begin) + int64_t(m_size); break; } case ESeekOrigin::Cur: { seek = 1; break; } }; if (nod_seek(m_reader.get(), seekOffset, seek) < 0) { return 0; } return nodReadLoop(m_reader.get(), buf, len); } u32 CDvdFile::SyncRead(void* buf, u32 len) { if (!m_reader) { return 0; } return nodReadLoop(m_reader.get(), buf, len); } std::string CDvdFile::NormalizePath(std::string_view path) { std::string out; out.reserve(path.size()); bool prevSlash = false; for (char c : path) { if (c == '/' || c == '\\') { if (!out.empty() && !prevSlash) { out.push_back('/'); } prevSlash = true; continue; } out.push_back(static_cast(std::tolower(static_cast(c)))); prevSlash = false; } if (!out.empty() && out.back() == '/') { out.pop_back(); } return out; } const CDvdFile::SFileEntry* CDvdFile::ResolvePath(std::string_view path) { if (!m_DataPartition) { return nullptr; } std::string normalizedPath = NormalizePath(path); if (normalizedPath.empty()) { return nullptr; } if (!m_rootDirectory.empty()) { normalizedPath = m_rootDirectory + "/" + normalizedPath; } const auto search = m_FileEntries.find(normalizedPath); return search != m_FileEntries.end() ? &search->second : nullptr; } bool CDvdFile::BuildFileEntries() { if (!m_DataPartition) { return false; } m_FileEntries.clear(); struct SDirFrame { u32 endIndex = 0; std::string path; }; struct SFstBuildContext { std::unordered_map* fileEntries = nullptr; std::vector dirStack; } ctx{&m_FileEntries, {}}; nod_partition_iterate_fst( m_DataPartition.get(), [](u32 index, NodNodeKind kind, const char* name, u32 size, void* userData) -> u32 { auto* ctx = static_cast(userData); while (!ctx->dirStack.empty() && index >= ctx->dirStack.back().endIndex) { ctx->dirStack.pop_back(); } const std::string nodeName = CDvdFile::NormalizePath(name != nullptr ? std::string_view{name} : std::string_view{}); if (nodeName.empty()) { return index + 1; } std::string fullPath; if (!ctx->dirStack.empty()) { fullPath = ctx->dirStack.back().path; fullPath += '/'; fullPath += nodeName; } else { fullPath = nodeName; } if (kind == NOD_NODE_KIND_FILE) { ctx->fileEntries->insert_or_assign(fullPath, CDvdFile::SFileEntry{index, size}); } else { ctx->dirStack.push_back({size, std::move(fullPath)}); } return index + 1; }, &ctx); return !m_FileEntries.empty(); } bool CDvdFile::LoadDolBuf() { if (!m_DataPartition) { return false; } NodPartitionMeta meta{}; if (nod_partition_meta(m_DataPartition.get(), &meta) != NOD_RESULT_OK || meta.raw_dol.data == nullptr || meta.raw_dol.size == 0) { return false; } auto dolBuf = std::make_unique(meta.raw_dol.size); std::memcpy(dolBuf.get(), meta.raw_dol.data, meta.raw_dol.size); m_dolBuf = std::move(dolBuf); m_dolBufLen = meta.raw_dol.size; return true; } bool CDvdFile::Initialize(const std::string_view& path) { Shutdown(); m_lastError.clear(); std::string pathStr(path); SDL_IOStream* io = SDL_IOFromFile(pathStr.c_str(), "rb"); if (io == nullptr) { m_lastError = std::string{"SDL_IOFromFile failed: "} + SDL_GetError(); spdlog::error("{} (path: '{}')", m_lastError, pathStr); return false; } auto* streamCtx = new (std::nothrow) SDLDiscStreamCtx{io}; if (streamCtx == nullptr) { SDL_CloseIO(io); m_lastError = "Failed to allocate SDL disc stream context"; spdlog::error("{} (path: '{}')", m_lastError, pathStr); return false; } NodHandle* discRaw = nullptr; const NodDiscOptions discOpts{ .preloader_threads = 1, }; const NodDiscStream stream{ .user_data = streamCtx, .read_at = sdlStreamReadAt, .stream_len = sdlStreamLen, .close = sdlStreamClose, }; const NodResult discResult = nod_disc_open_stream(&stream, &discOpts, &discRaw); if (discResult != NOD_RESULT_OK || discRaw == nullptr) { const char* nodError = nod_error_message(); m_lastError = fmt::format("nod_disc_open_stream failed ({}){}", int(discResult), nodError != nullptr && nodError[0] != '\0' ? fmt::format(": {}", nodError) : ""); spdlog::error("{} (path: '{}')", m_lastError, pathStr); // Ownership of streamCtx is transferred to nod_disc_open_stream. // FfiDiscStream drops and invokes close() on failure paths. return false; } m_DvdRoot = NodHandleUnique(discRaw, nod_free); NodHandle* partitionRaw = nullptr; const NodResult partitionResult = nod_disc_open_partition_kind(m_DvdRoot.get(), NOD_PARTITION_KIND_DATA, nullptr, &partitionRaw); if (partitionResult != NOD_RESULT_OK || partitionRaw == nullptr) { const char* nodError = nod_error_message(); m_lastError = fmt::format("nod_disc_open_partition_kind(data) failed ({}){}", int(partitionResult), nodError != nullptr && nodError[0] != '\0' ? fmt::format(": {}", nodError) : ""); spdlog::error("{} (path: '{}')", m_lastError, pathStr); Shutdown(); return false; } m_DataPartition = NodHandleUnique(partitionRaw, nod_free); if (!BuildFileEntries()) { m_lastError = "Failed to read disc file-system table"; spdlog::error("{} (path: '{}')", m_lastError, pathStr); Shutdown(); return false; } if (!LoadDolBuf()) { m_lastError = "Failed to load raw DOL data from disc"; spdlog::error("{} (path: '{}')", m_lastError, pathStr); Shutdown(); return false; } return true; } void CDvdFile::Shutdown() { m_RequestQueue.clear(); m_FileEntries.clear(); m_dolBuf.reset(); m_dolBufLen = 0; m_DataPartition.reset(); m_DvdRoot.reset(); } SDiscInfo CDvdFile::DiscInfo() { SDiscInfo out{}; if (!m_DvdRoot) { return out; } NodDiscHeader header{}; if (nod_disc_header(m_DvdRoot.get(), &header) != NOD_RESULT_OK) { return out; } std::memcpy(out.gameId.data(), header.game_id, sizeof(header.game_id)); out.version = header.disc_version; const char* titleBegin = header.game_title; const char* titleEnd = std::find(titleBegin, titleBegin + sizeof(header.game_title), '\0'); out.gameTitle.assign(titleBegin, titleEnd); return out; } void CDvdFile::SetRootDirectory(const std::string_view& rootDir) { m_rootDirectory = NormalizePath(rootDir); } } // namespace metaforce