aboutsummaryrefslogtreecommitdiff
path: root/src/video_core/host1x/nx2_video_decoder.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/video_core/host1x/nx2_video_decoder.cpp')
-rw-r--r--src/video_core/host1x/nx2_video_decoder.cpp193
1 files changed, 193 insertions, 0 deletions
diff --git a/src/video_core/host1x/nx2_video_decoder.cpp b/src/video_core/host1x/nx2_video_decoder.cpp
new file mode 100644
index 0000000000..7d185ca573
--- /dev/null
+++ b/src/video_core/host1x/nx2_video_decoder.cpp
@@ -0,0 +1,193 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <array>
+#include <cstring>
+#include <optional>
+#include <set>
+
+#include "common/logging.h"
+#include "common/settings.h"
+#include "video_core/host1x/ffmpeg.h"
+#include "video_core/host1x/nx2_video_decoder.h"
+
+extern "C" {
+#if defined(__GNUC__) || defined(__clang__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wconversion"
+#endif
+
+#include <libswscale/swscale.h>
+
+#if defined(__GNUC__) || defined(__clang__)
+#pragma GCC diagnostic pop
+#endif
+}
+
+namespace Tegra::Host1x {
+
+namespace {
+
+std::optional<NvdecCommon::VideoCodec> CodecFromMime(std::string_view mime) {
+ if (mime.find("vp9") != std::string_view::npos) {
+ return NvdecCommon::VideoCodec::VP9;
+ }
+ if (mime.find("vp8") != std::string_view::npos) {
+ return NvdecCommon::VideoCodec::VP8;
+ }
+ if (mime.find("avc") != std::string_view::npos || mime.find("264") != std::string_view::npos) {
+ return NvdecCommon::VideoCodec::H264;
+ }
+ return std::nullopt;
+}
+
+} // namespace
+
+struct Nx2VideoDecoder::Impl {
+ std::string name;
+ std::optional<FFmpeg::Decoder> decoder;
+ std::optional<FFmpeg::HardwareContext> hardware;
+ std::optional<FFmpeg::DecoderContext> context;
+ SwsContext* scaler{};
+ bool open{};
+ /// Timestamps of submitted frames not yet output. Frames copied back from a host GPU decoder
+ /// lose theirs and take the earliest one instead.
+ std::multiset<u64> pending_pts;
+ std::vector<u8> packet; ///< FFmpeg reads up to AV_INPUT_BUFFER_PADDING_SIZE past the data
+
+ ~Impl() {
+ sws_freeContext(scaler);
+ context.reset();
+ hardware.reset();
+ }
+
+ void Receive(std::vector<Nx2Picture>& pictures) {
+ while (auto frame = context->ReceiveFrame()) {
+ const AVFrame* f = frame->GetFrame();
+ const s64 stamp = f->best_effort_timestamp != AV_NOPTS_VALUE ? f->best_effort_timestamp
+ : f->pts;
+ u64 pts{};
+ if (stamp != AV_NOPTS_VALUE) {
+ pts = static_cast<u64>(stamp);
+ if (const auto it = pending_pts.find(pts); it != pending_pts.end()) {
+ pending_pts.erase(it);
+ }
+ } else if (!pending_pts.empty()) {
+ pts = *pending_pts.begin();
+ pending_pts.erase(pending_pts.begin());
+ }
+ pictures.push_back({static_cast<u32>(f->width), static_cast<u32>(f->height), pts,
+ std::move(frame)});
+ }
+ }
+};
+
+Nx2VideoDecoder::Nx2VideoDecoder(std::string_view mime) : impl{std::make_unique<Impl>()} {
+ impl->name = std::string{mime};
+ const auto codec = CodecFromMime(mime);
+ if (!codec) {
+ LOG_ERROR(HW_GPU, "nx2 video: no decoder for codec \"{}\"", mime);
+ return;
+ }
+ impl->decoder.emplace(*codec);
+ if (impl->decoder->GetCodec() == nullptr) {
+ LOG_ERROR(HW_GPU, "nx2 video: FFmpeg has no decoder for \"{}\"", mime);
+ return;
+ }
+ impl->context.emplace(*impl->decoder);
+ if (Settings::values.nvdec_emulation.GetValue() == Settings::NvdecEmulation::Gpu) {
+ impl->hardware.emplace();
+ impl->hardware->InitializeForDecoder(*impl->context, *impl->decoder);
+ }
+ impl->open = impl->context->OpenContext(*impl->decoder);
+ LOG_INFO(HW_GPU, "nx2 video: decoder for \"{}\" {}", mime, impl->open ? "open" : "failed to open");
+}
+
+Nx2VideoDecoder::~Nx2VideoDecoder() = default;
+
+bool Nx2VideoDecoder::IsOpen() const {
+ return impl->open;
+}
+
+const std::string& Nx2VideoDecoder::Name() const {
+ return impl->name;
+}
+
+void Nx2VideoDecoder::Decode(std::span<const u8> data, u64 pts, std::vector<Nx2Picture>& pictures) {
+ if (!impl->open || data.empty()) {
+ return;
+ }
+ impl->packet.assign(data.size() + AV_INPUT_BUFFER_PADDING_SIZE, 0);
+ std::memcpy(impl->packet.data(), data.data(), data.size());
+ FFmpeg::Packet packet{std::span<const u8>{impl->packet.data(), data.size()}};
+ packet.GetPacket()->pts = static_cast<s64>(pts);
+ // Hidden VP9 frames never come out: keep the set from growing over a long movie.
+ if (impl->pending_pts.size() >= 64) {
+ impl->pending_pts.erase(impl->pending_pts.begin());
+ }
+ impl->pending_pts.insert(pts);
+ impl->context->SendPacket(packet);
+ impl->Receive(pictures);
+}
+
+void Nx2VideoDecoder::Drain(std::vector<Nx2Picture>& pictures) {
+ if (!impl->open) {
+ return;
+ }
+ avcodec_send_packet(impl->context->GetCodecContext(), nullptr);
+ impl->Receive(pictures);
+ Reset();
+}
+
+void Nx2VideoDecoder::Reset() {
+ if (impl->open) {
+ avcodec_flush_buffers(impl->context->GetCodecContext());
+ }
+ impl->pending_pts.clear();
+}
+
+void Nx2VideoDecoder::Render(const Nx2Picture& picture, Nx2PictureFormat format, u32 width,
+ u32 height, u64 pitch, u64 chroma_offset, std::span<u8> out) {
+ const AVFrame* f = picture.frame->GetFrame();
+ const u64 needed = format == Nx2PictureFormat::Nv12
+ ? chroma_offset + pitch * ((u64{height} + 1) / 2)
+ : pitch * height;
+ if (width == 0 || height == 0 || out.size() < needed) {
+ return;
+ }
+ const AVPixelFormat target = format == Nx2PictureFormat::Nv12 ? AV_PIX_FMT_NV12
+ : format == Nx2PictureFormat::Rgba8 ? AV_PIX_FMT_RGBA
+ : AV_PIX_FMT_BGRA;
+ impl->scaler = sws_getCachedContext(impl->scaler, f->width, f->height,
+ static_cast<AVPixelFormat>(f->format),
+ static_cast<int>(width), static_cast<int>(height), target,
+ SWS_BILINEAR, nullptr, nullptr, nullptr);
+ if (impl->scaler == nullptr) {
+ LOG_ERROR(HW_GPU, "nx2 video: cannot convert {} to {}",
+ av_get_pix_fmt_name(static_cast<AVPixelFormat>(f->format)),
+ av_get_pix_fmt_name(target));
+ return;
+ }
+ if (format != Nx2PictureFormat::Nv12) {
+ // The stream's matrix and range; untagged HD video is BT.709.
+ int space = SWS_CS_ITU601;
+ if (f->colorspace == AVCOL_SPC_BT709 ||
+ (f->colorspace == AVCOL_SPC_UNSPECIFIED && f->height >= 720)) {
+ space = SWS_CS_ITU709;
+ } else if (f->colorspace == AVCOL_SPC_BT2020_NCL || f->colorspace == AVCOL_SPC_BT2020_CL) {
+ space = SWS_CS_BT2020;
+ }
+ const int* coefficients = sws_getCoefficients(space);
+ sws_setColorspaceDetails(impl->scaler, coefficients, f->color_range == AVCOL_RANGE_JPEG,
+ coefficients, 1, 0, 1 << 16, 1 << 16);
+ }
+ std::array<u8*, 4> planes{out.data(), nullptr, nullptr, nullptr};
+ std::array<int, 4> strides{static_cast<int>(pitch), 0, 0, 0};
+ if (format == Nx2PictureFormat::Nv12) {
+ planes[1] = out.data() + chroma_offset;
+ strides[1] = static_cast<int>(pitch);
+ }
+ sws_scale(impl->scaler, f->data, f->linesize, 0, f->height, planes.data(), strides.data());
+}
+
+} // namespace Tegra::Host1x