diff options
| author | CamilleLaVey <camillelavey99@gmail.com> | 2026-07-09 06:55:54 +0200 |
|---|---|---|
| committer | crueter <crueter@eden-emu.dev> | 2026-07-09 06:55:54 +0200 |
| commit | 372bdfccbc06aead40fc4d14620df6c292728656 (patch) | |
| tree | 339492949dca538f67e64836c6bf1687bbc0f5e4 /src/video_core/host1x | |
| parent | 3699795315a1fa4d47542f70da0c6c6e529697e6 (diff) | |
[vulkan, android] Mediacodec implementation + Vulkan fixes (#4191)
This is the first step into the conversion on full GPU video decoding for Android; currently due to the VIC structure, I couldn't set it on surface mode to prevent the latency when sending video decoded (which is processed by GPU now), because currently we convert YUV420 into the Nvidia's format each frame constantly on CPU, aside the requirements from other extensions to work + VIC rewrite, which is a work currently not planned to happen on this PR; the actual configuration for video decoding is ByteBuffer, using GPU to decode NVDEC data (all codecs supported, h264, vp8 and vp9) and send it to CPU for display purposes, which is more faster than relying purely on CPU for any drawing task, saving devices resources/ heating, the downside on this will be the slight latency when a new frame is displayed, which is gonna be a black frame for less than a second, nothing major to harm the experience rather than actually trying our best to take advantage on hardware accelerated.
Aside this, I also added a bunch of minor Vulkan fixes to grant drivers less thinkering when receiving spir-v instructions, meaning this has new bans for extensions on QCOM (following reported issues on other projects working around Adreno driver behavior), this more than providing performance aims to enhance the stability on the driver, performance it's gonna likely to be hit based on the UBO's (StorageBufferAccess) operations and SSBO (uniformStorageBufferAccess), there was an already existing path for the emulation which forces to wide them into 32bit packed operations. I also included some smaller changes/ bugs + VUID's fixes from earlier changes on my work.
Special Thanks:
-> Mr. Smoly Gidolard (@gidoly)
Sources:
1.- https://github.com/microsoft/DirectXShaderCompiler/issues/2842
2.- https://github.com/mlc-ai/web-llm/issues/836
3.- https://github.com/ggml-org/llama.cpp/issues/5186
4.- https://github.com/encounter/aurora/pull/202
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4191
Diffstat (limited to 'src/video_core/host1x')
| -rw-r--r-- | src/video_core/host1x/codecs/decoder.cpp | 66 | ||||
| -rw-r--r-- | src/video_core/host1x/codecs/decoder.h | 8 | ||||
| -rw-r--r-- | src/video_core/host1x/codecs/h264.cpp | 4 | ||||
| -rw-r--r-- | src/video_core/host1x/codecs/vp8.cpp | 1 | ||||
| -rw-r--r-- | src/video_core/host1x/codecs/vp9.cpp | 1 | ||||
| -rw-r--r-- | src/video_core/host1x/ffmpeg.cpp | 169 | ||||
| -rw-r--r-- | src/video_core/host1x/ffmpeg.h | 29 | ||||
| -rw-r--r-- | src/video_core/host1x/nvdec.cpp | 1 | ||||
| -rw-r--r-- | src/video_core/host1x/vic.cpp | 9 | ||||
| -rw-r--r-- | src/video_core/host1x/vic.h | 1 |
10 files changed, 240 insertions, 49 deletions
diff --git a/src/video_core/host1x/codecs/decoder.cpp b/src/video_core/host1x/codecs/decoder.cpp index c75059db6f..1d6d3e8af6 100644 --- a/src/video_core/host1x/codecs/decoder.cpp +++ b/src/video_core/host1x/codecs/decoder.cpp @@ -20,58 +20,54 @@ Decoder::Decoder(Host1x::Host1x& host1x_, s32 id_, const Host1x::NvdecCommon::Nv Decoder::~Decoder() = default; -void Decoder::Decode() { - if (!initialized) { +void Decoder::SetFrameDimensions(s32 width, s32 height) { + if (width <= 0 || height <= 0) { + frame_dimensions.reset(); return; } + frame_dimensions = FFmpeg::FrameDimensions{width, height}; +} - const auto packet_data = ComposeFrame(); - // Send assembled bitstream to decoder. - if (!decode_api.SendPacket(packet_data)) { +void Decoder::Decode() { + if (!initialized) { return; } - // Only receive/store visible frames. - if (vp9_hidden_frame) { - return; + const auto packet_data = ComposeFrame(); + FFmpeg::FrameOffsets offsets{}; + offsets.hidden = vp9_hidden_frame; + offsets.interlaced = IsInterlaced(); + if (offsets.interlaced) { + std::tie(offsets.luma, offsets.luma_bottom, std::ignore, std::ignore) = + GetInterlacedOffsets(); + } else { + std::tie(offsets.luma, std::ignore) = GetProgressiveOffsets(); } - // Receive output frames from decoder. - auto frame = decode_api.ReceiveFrame(); - - if (!frame) { + // Send assembled bitstream to decoder. + if (!decode_api.SendPacket(packet_data, offsets, GetFrameDimensions())) { return; } - if (IsInterlaced()) { - auto [luma_top, luma_bottom, chroma_top, chroma_bottom] = GetInterlacedOffsets(); - auto frame_copy = frame; - - if (!frame.get()) { - LOG_ERROR(HW_GPU, - "Nvdec {} failed to decode interlaced frame for top {:#X} bottom 0x{:X}", id, - luma_top, luma_bottom); - } - + auto push = [&](u64 luma, std::shared_ptr<FFmpeg::Frame> frame) { if (UsingDecodeOrder()) { - host1x.frame_queue.PushDecodeOrder(id, luma_top, std::move(frame)); - host1x.frame_queue.PushDecodeOrder(id, luma_bottom, std::move(frame_copy)); + host1x.frame_queue.PushDecodeOrder(id, luma, std::move(frame)); } else { - host1x.frame_queue.PushPresentOrder(id, luma_top, std::move(frame)); - host1x.frame_queue.PushPresentOrder(id, luma_bottom, std::move(frame_copy)); + host1x.frame_queue.PushPresentOrder(id, luma, std::move(frame)); } - } else { - auto [luma_offset, chroma_offset] = GetProgressiveOffsets(); + }; - if (!frame.get()) { - LOG_ERROR(HW_GPU, "Nvdec {} failed to decode progressive frame for luma {:#X}", id, - luma_offset); + while (auto result = decode_api.ReceiveFrame()) { + auto& [frame, frame_offsets] = *result; + if (!frame) { + continue; } - - if (UsingDecodeOrder()) { - host1x.frame_queue.PushDecodeOrder(id, luma_offset, std::move(frame)); + if (frame_offsets.interlaced) { + auto frame_copy = frame; + push(frame_offsets.luma, std::move(frame)); + push(frame_offsets.luma_bottom, std::move(frame_copy)); } else { - host1x.frame_queue.PushPresentOrder(id, luma_offset, std::move(frame)); + push(frame_offsets.luma, std::move(frame)); } } } diff --git a/src/video_core/host1x/codecs/decoder.h b/src/video_core/host1x/codecs/decoder.h index 9fca89aa40..2cc6d8dce6 100644 --- a/src/video_core/host1x/codecs/decoder.h +++ b/src/video_core/host1x/codecs/decoder.h @@ -10,6 +10,7 @@ #include <mutex> #include <optional> #include <string_view> +#include <tuple> #include <ankerl/unordered_dense.h> #include <queue> @@ -46,12 +47,19 @@ protected: virtual std::tuple<u64, u64, u64, u64> GetInterlacedOffsets() = 0; virtual bool IsInterlaced() = 0; + void SetFrameDimensions(s32 width, s32 height); + + std::optional<FFmpeg::FrameDimensions> GetFrameDimensions() const { + return frame_dimensions; + } + FFmpeg::DecodeApi decode_api; Host1x::Host1x& host1x; const Host1x::NvdecCommon::NvdecRegisters& regs; s32 id; bool initialized : 1 = false; bool vp9_hidden_frame : 1 = false; + std::optional<FFmpeg::FrameDimensions> frame_dimensions; }; } // namespace Tegra diff --git a/src/video_core/host1x/codecs/h264.cpp b/src/video_core/host1x/codecs/h264.cpp index f439ac3828..e3a6f5d182 100644 --- a/src/video_core/host1x/codecs/h264.cpp +++ b/src/video_core/host1x/codecs/h264.cpp @@ -52,6 +52,10 @@ bool H264::IsInterlaced() { std::span<const u8> H264::ComposeFrame() { host1x.gmmu_manager.ReadBlock(regs.picture_info_offset.Address(), ¤t_context, sizeof(H264DecoderContext)); + const auto& params = current_context.h264_parameter_set; + SetFrameDimensions(static_cast<s32>(params.pic_width_in_mbs) * 16, + static_cast<s32>(params.frame_height_in_mbs) * 16); + const s64 frame_number = current_context.h264_parameter_set.frame_number.Value(); if (!is_first_frame && frame_number != 0) { frame_scratch.resize_destructive(current_context.stream_len); diff --git a/src/video_core/host1x/codecs/vp8.cpp b/src/video_core/host1x/codecs/vp8.cpp index 00fe6e4499..33d781816a 100644 --- a/src/video_core/host1x/codecs/vp8.cpp +++ b/src/video_core/host1x/codecs/vp8.cpp @@ -35,6 +35,7 @@ std::tuple<u64, u64, u64, u64> VP8::GetInterlacedOffsets() { std::span<const u8> VP8::ComposeFrame() { host1x.gmmu_manager.ReadBlock(regs.picture_info_offset.Address(), ¤t_context, sizeof(VP8PictureInfo)); + SetFrameDimensions(current_context.frame_width, current_context.frame_height); const bool is_key_frame = current_context.key_frame == 1u; const auto bitstream_size = size_t(current_context.vld_buffer_size); diff --git a/src/video_core/host1x/codecs/vp9.cpp b/src/video_core/host1x/codecs/vp9.cpp index 1085e08939..dcc6261e5c 100644 --- a/src/video_core/host1x/codecs/vp9.cpp +++ b/src/video_core/host1x/codecs/vp9.cpp @@ -841,6 +841,7 @@ std::span<const u8> VP9::ComposeFrame() { { Vp9FrameContainer curr_frame = GetCurrentFrame(); current_frame_info = curr_frame.info; + SetFrameDimensions(current_frame_info.frame_size.width, current_frame_info.frame_size.height); bitstream = std::move(curr_frame.bit_stream); } // The uncompressed header routine sets PrevProb parameters needed for the compressed header diff --git a/src/video_core/host1x/ffmpeg.cpp b/src/video_core/host1x/ffmpeg.cpp index 88df6695d8..783259ccfc 100644 --- a/src/video_core/host1x/ffmpeg.cpp +++ b/src/video_core/host1x/ffmpeg.cpp @@ -4,6 +4,10 @@ // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include <cstring> +#include <string_view> +#include <vector> + #include "common/assert.h" #include "common/logging.h" #include "common/scope_exit.h" @@ -84,6 +88,52 @@ std::string AVError(int errnum) { return errbuf; } +#if defined(__ANDROID__) +size_t FindNalStartCode(std::span<const u8> data, size_t i) { + const size_t n = data.size(); + if (i + 3 < n && data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 && data[i + 3] == 1) { + return 4; + } + if (i + 2 < n && data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1) { + return 3; + } + return 0; +} + +std::vector<u8> ExtractH264ParameterSetExtradata(std::span<const u8> packet) { + std::vector<u8> extradata; + const size_t size = packet.size(); + size_t i = 0; + while (i < size) { + const size_t sc = FindNalStartCode(packet, i); + if (sc == 0) { + ++i; + continue; + } + const size_t nal_start = i + sc; + if (nal_start >= size) { + break; + } + const u8 nal_type = packet[nal_start] & 0x1F; + + size_t j = nal_start + 1; + while (j < size && FindNalStartCode(packet, j) == 0) { + ++j; + } + + if (nal_type == 7 || nal_type == 8) { + constexpr u8 start[4] = {0, 0, 0, 1}; + extradata.insert(extradata.end(), start, start + sizeof(start)); + extradata.insert(extradata.end(), packet.begin() + nal_start, packet.begin() + j); + } else if (nal_type == 1 || nal_type == 5) { + break; + } + i = j; + } + return extradata; +} +#endif + } Packet::Packet(std::span<const u8> data) { @@ -118,7 +168,24 @@ Decoder::Decoder(Tegra::Host1x::NvdecCommon::VideoCodec codec) { return AV_CODEC_ID_NONE; } }(); - m_codec = avcodec_find_decoder(av_codec); + +#if defined(__ANDROID__) + if (Settings::values.nvdec_emulation.GetValue() == Settings::NvdecEmulation::Gpu) { + const char* mc_name = nullptr; + switch (av_codec) { + case AV_CODEC_ID_H264: mc_name = "h264_mediacodec"; break; + case AV_CODEC_ID_VP8: mc_name = "vp8_mediacodec"; break; + case AV_CODEC_ID_VP9: mc_name = "vp9_mediacodec"; break; + default: break; + } + if (mc_name) { + m_codec = avcodec_find_decoder_by_name(mc_name); + } + } +#endif + if (!m_codec) { + m_codec = avcodec_find_decoder(av_codec); + } } bool Decoder::SupportsDecodingOnDevice(AVPixelFormat* out_pix_fmt, AVHWDeviceType type) const { @@ -214,6 +281,10 @@ DecoderContext::DecoderContext(const Decoder& decoder) : m_decoder{decoder} { av_opt_set(m_codec_context->priv_data, "tune", "zerolatency", 0); m_codec_context->thread_count = 0; m_codec_context->thread_type &= ~FF_THREAD_FRAME; +#if defined(__ANDROID__) + m_codec_context->flags |= AV_CODEC_FLAG_LOW_DELAY; + m_codec_context->flags2 |= AV_CODEC_FLAG2_FAST; +#endif } DecoderContext::~DecoderContext() { @@ -227,15 +298,25 @@ void DecoderContext::InitializeHardwareDecoder(const HardwareContext& context, A m_codec_context->pix_fmt = hw_pix_fmt; } -bool DecoderContext::OpenContext(const Decoder& decoder) { +bool DecoderContext::OpenContext(const Decoder& decoder, std::span<const u8> extradata) { + if (!extradata.empty()) { + av_freep(&m_codec_context->extradata); + m_codec_context->extradata = static_cast<u8*>( + av_mallocz(extradata.size() + AV_INPUT_BUFFER_PADDING_SIZE)); + if (!m_codec_context->extradata) { + LOG_ERROR(HW_GPU, "Failed to allocate extradata"); + return false; + } + std::memcpy(m_codec_context->extradata, extradata.data(), extradata.size()); + m_codec_context->extradata_size = static_cast<int>(extradata.size()); + } + if (const int ret = avcodec_open2(m_codec_context, decoder.GetCodec(), nullptr); ret < 0) { LOG_ERROR(HW_GPU, "avcodec_open2 error: {}", AVError(ret)); return false; } - if (!m_codec_context->hw_device_ctx) { - LOG_INFO(HW_GPU, "Using FFmpeg CPU decoder"); - } + LOG_INFO(HW_GPU, "Using decoder {}", decoder.GetCodec()->name); return true; } @@ -281,6 +362,13 @@ void DecodeApi::Reset() { m_hardware_context.reset(); m_decoder_context.reset(); m_decoder.reset(); + m_opened = false; + m_defer_android_mediacodec_open = false; + m_needs_h264_extradata = false; + m_next_pts = 0; + while (!m_pending_offsets.empty()) { + m_pending_offsets.pop(); + } } bool DecodeApi::Initialize(Tegra::Host1x::NvdecCommon::VideoCodec codec) { @@ -288,29 +376,90 @@ bool DecodeApi::Initialize(Tegra::Host1x::NvdecCommon::VideoCodec codec) { m_decoder.emplace(codec); m_decoder_context.emplace(*m_decoder); + bool is_mediacodec = false; +#if defined(__ANDROID__) + const std::string_view decoder_name = m_decoder->GetCodec() ? m_decoder->GetCodec()->name : ""; + is_mediacodec = decoder_name == "h264_mediacodec" || + decoder_name == "vp8_mediacodec" || + decoder_name == "vp9_mediacodec"; +#endif + // Enable GPU decoding if requested. - if (Settings::values.nvdec_emulation.GetValue() == Settings::NvdecEmulation::Gpu) { + if (!is_mediacodec && + Settings::values.nvdec_emulation.GetValue() == Settings::NvdecEmulation::Gpu) { m_hardware_context.emplace(); m_hardware_context->InitializeForDecoder(*m_decoder_context, *m_decoder); } +#if defined(__ANDROID__) + m_defer_android_mediacodec_open = is_mediacodec; + m_needs_h264_extradata = decoder_name == "h264_mediacodec"; + if (m_defer_android_mediacodec_open) { + return true; + } +#endif + // Open the decoder context. if (!m_decoder_context->OpenContext(*m_decoder)) { this->Reset(); return false; } + m_opened = true; return true; } -bool DecodeApi::SendPacket(std::span<const u8> packet_data) { +bool DecodeApi::SendPacket(std::span<const u8> packet_data, const FrameOffsets& offsets, + std::optional<FrameDimensions> dimensions) { + if (!m_opened) { + std::vector<u8> extradata; +#if defined(__ANDROID__) + if (m_defer_android_mediacodec_open) { + if (!dimensions) { + return true; + } + + auto* ctx = m_decoder_context->GetCodecContext(); + ctx->width = dimensions->width; + ctx->height = dimensions->height; + ctx->coded_width = dimensions->width; + ctx->coded_height = dimensions->height; + } + + if (m_needs_h264_extradata) { + extradata = ExtractH264ParameterSetExtradata(packet_data); + if (extradata.empty()) { + return true; + } + } +#endif + if (!m_decoder_context->OpenContext(*m_decoder, extradata)) { + this->Reset(); + return false; + } + m_opened = true; + } + if (!offsets.hidden) { + m_pending_offsets.push(offsets); + } FFmpeg::Packet packet(packet_data); + packet.GetPacket()->pts = m_next_pts; + packet.GetPacket()->dts = m_next_pts; + ++m_next_pts; return m_decoder_context->SendPacket(packet); } -std::shared_ptr<Frame> DecodeApi::ReceiveFrame() { - // Receive raw frame from decoder. - return m_decoder_context->ReceiveFrame(); +std::optional<DecodeApi::DecodedFrame> DecodeApi::ReceiveFrame() { + auto frame = m_decoder_context->ReceiveFrame(); + if (!frame) { + return std::nullopt; + } + FrameOffsets offsets{}; + if (!m_pending_offsets.empty()) { + offsets = m_pending_offsets.front(); + m_pending_offsets.pop(); + } + return DecodedFrame{std::move(frame), offsets}; } } diff --git a/src/video_core/host1x/ffmpeg.h b/src/video_core/host1x/ffmpeg.h index fdb6908bb6..00cd3f5d4d 100644 --- a/src/video_core/host1x/ffmpeg.h +++ b/src/video_core/host1x/ffmpeg.h @@ -179,7 +179,7 @@ public: ~DecoderContext(); void InitializeHardwareDecoder(const HardwareContext& context, AVPixelFormat hw_pix_fmt); - bool OpenContext(const Decoder& decoder); + bool OpenContext(const Decoder& decoder, std::span<const u8> extradata = {}); bool SendPacket(const Packet& packet); std::shared_ptr<Frame> ReceiveFrame(); @@ -198,6 +198,18 @@ private: bool m_decode_order{}; }; +struct FrameOffsets { + bool interlaced{}; + bool hidden{}; + u64 luma{}; + u64 luma_bottom{}; +}; + +struct FrameDimensions { + s32 width{}; + s32 height{}; +}; + class DecodeApi { public: YUZU_NON_COPYABLE(DecodeApi); @@ -213,13 +225,24 @@ public: return m_decoder_context->UsingDecodeOrder(); } - bool SendPacket(std::span<const u8> packet_data); - std::shared_ptr<Frame> ReceiveFrame(); + bool SendPacket(std::span<const u8> packet_data, const FrameOffsets& offsets, + std::optional<FrameDimensions> dimensions = std::nullopt); + + struct DecodedFrame { + std::shared_ptr<Frame> frame; + FrameOffsets offsets; + }; + std::optional<DecodedFrame> ReceiveFrame(); private: std::optional<FFmpeg::Decoder> m_decoder; std::optional<FFmpeg::DecoderContext> m_decoder_context; std::optional<FFmpeg::HardwareContext> m_hardware_context; + bool m_opened{}; + bool m_defer_android_mediacodec_open{}; + bool m_needs_h264_extradata{}; + s64 m_next_pts{}; + std::queue<FrameOffsets> m_pending_offsets; }; } // namespace FFmpeg diff --git a/src/video_core/host1x/nvdec.cpp b/src/video_core/host1x/nvdec.cpp index f2e5c358d8..5dbc6a417e 100644 --- a/src/video_core/host1x/nvdec.cpp +++ b/src/video_core/host1x/nvdec.cpp @@ -31,6 +31,7 @@ Nvdec::Nvdec(Host1x& host1x_, s32 id_, u32 syncpt) Nvdec::~Nvdec() { LOG_INFO(HW_GPU, "Destroying nvdec {}", id); + host1x.frame_queue.Close(id); } void Nvdec::ProcessMethod(u32 method, u32 argument) { diff --git a/src/video_core/host1x/vic.cpp b/src/video_core/host1x/vic.cpp index f7830a3a45..6514508947 100644 --- a/src/video_core/host1x/vic.cpp +++ b/src/video_core/host1x/vic.cpp @@ -118,6 +118,7 @@ void Vic::Execute() noexcept { output_surface.resize(output_width * output_height); if (Settings::values.nvdec_emulation.GetValue() != Settings::NvdecEmulation::Off) { + bool decoded_frame = false; for (size_t i = 0; i < config.slot_structs.size(); i++) { if (auto& slot_config = config.slot_structs[i]; slot_config.config.slot_enable) { auto const luma_offset = regs.surfaces[i][SurfaceIndex::Current].luma.Address(); @@ -136,11 +137,17 @@ void Vic::Execute() noexcept { break; } Blend(config, slot_config, config.output_surface_config.out_pixel_format); + decoded_frame = true; } else { - LOG_ERROR(HW_GPU, "Vic {} failed to get frame with offset {:#X}", id, luma_offset); + LOG_TRACE(HW_GPU, "Vic {} failed to get frame with offset {:#X}", id, luma_offset); } } } + if (decoded_frame) { + has_decoded_frame = true; + } else if (!has_decoded_frame) { + return; + } } else { // Fill the frame with black, as otherwise they can have random data and be very glitchy. std::fill(output_surface.begin(), output_surface.end(), Pixel{}); diff --git a/src/video_core/host1x/vic.h b/src/video_core/host1x/vic.h index 23f3f66c18..18eff3382f 100644 --- a/src/video_core/host1x/vic.h +++ b/src/video_core/host1x/vic.h @@ -628,6 +628,7 @@ private: s32 id; s32 nvdec_id{-1}; + bool has_decoded_frame{}; u32 syncpoint; }; |
