17 changed files with 577 additions and 0 deletions
-
162src/android/app/src/main/java/org/yuzu/yuzu_emu/media/NativeMediaCodec.java
-
7src/common/android/id_cache.cpp
-
6src/video_core/CMakeLists.txt
-
5src/video_core/host1x/codecs/decoder.cpp
-
6src/video_core/host1x/codecs/decoder.h
-
16src/video_core/host1x/codecs/h264.cpp
-
3src/video_core/host1x/codecs/h264.h
-
9src/video_core/host1x/codecs/vp8.cpp
-
3src/video_core/host1x/codecs/vp8.h
-
9src/video_core/host1x/codecs/vp9.cpp
-
3src/video_core/host1x/codecs/vp9.h
-
132src/video_core/host1x/ffmpeg/ffmpeg.cpp
-
10src/video_core/host1x/ffmpeg/ffmpeg.h
-
22src/video_core/host1x/ffmpeg/mediacodec_bridge.h
-
114src/video_core/host1x/ffmpeg/mediacodec_bridge_android.cpp
-
19src/video_core/vulkan_common/vulkan_device.cpp
-
51src/video_core/vulkan_common/vulkan_device.h
@ -0,0 +1,162 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
|
||||
|
package org.yuzu.yuzu_emu.media; |
||||
|
|
||||
|
import android.media.Image; |
||||
|
import android.media.MediaCodec; |
||||
|
import android.media.MediaCodecInfo; |
||||
|
import android.media.MediaFormat; |
||||
|
import android.os.Build; |
||||
|
import android.util.Log; |
||||
|
|
||||
|
import java.nio.ByteBuffer; |
||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||
|
import java.util.concurrent.atomic.AtomicInteger; |
||||
|
|
||||
|
public class NativeMediaCodec { |
||||
|
private static final String TAG = "NativeMediaCodec"; |
||||
|
private static final ConcurrentHashMap<Integer, MediaCodec> decoders = new ConcurrentHashMap<>(); |
||||
|
private static final AtomicInteger nextId = new AtomicInteger(1); |
||||
|
|
||||
|
// Called from native code to create a decoder for the given mime (e.g. "video/avc"). |
||||
|
// Returns a decoder id (>0) on success, or 0 on failure. |
||||
|
public static int createDecoder(String mime, int width, int height) { |
||||
|
try { |
||||
|
MediaCodec codec = MediaCodec.createDecoderByType(mime); |
||||
|
MediaFormat format = MediaFormat.createVideoFormat(mime, width, height); |
||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { |
||||
|
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, |
||||
|
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible); |
||||
|
} |
||||
|
final int id = nextId.getAndIncrement(); |
||||
|
decoders.put(id, codec); |
||||
|
// Request YUV_420_888 output (Image) if available |
||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
||||
|
codec.setCallback(new MediaCodec.Callback() { |
||||
|
private final int decoderId = id; |
||||
|
|
||||
|
@Override |
||||
|
public void onInputBufferAvailable(MediaCodec mc, int index) { |
||||
|
// input will be fed by native code via dequeue |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onOutputBufferAvailable(MediaCodec mc, int index, MediaCodec.BufferInfo info) { |
||||
|
try { |
||||
|
Image image = mc.getOutputImage(index); |
||||
|
if (image != null) { |
||||
|
byte[] data = ImageToNV12(image); |
||||
|
onFrameDecoded(decoderId, data, image.getWidth(), image.getHeight(), info.presentationTimeUs); |
||||
|
image.close(); |
||||
|
} |
||||
|
} catch (Throwable t) { |
||||
|
Log.w(TAG, "onOutputBufferAvailable failed: " + t); |
||||
|
} finally { |
||||
|
try { mc.releaseOutputBuffer(index, false); } catch (Throwable ignored) {} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onError(MediaCodec mc, MediaCodec.CodecException e) { |
||||
|
Log.w(TAG, "MediaCodec error: " + e); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onOutputFormatChanged(MediaCodec mc, MediaFormat format) { |
||||
|
Log.i(TAG, "Output format changed: " + format); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
codec.configure(format, null, null, 0); |
||||
|
codec.start(); |
||||
|
return id; |
||||
|
} catch (Exception e) { |
||||
|
Log.w(TAG, "createDecoder failed: " + e); |
||||
|
return 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static byte[] ImageToNV12(Image image) { |
||||
|
// Convert YUV_420_888 to NV12 (Y plane, interleaved UV) |
||||
|
final Image.Plane[] planes = image.getPlanes(); |
||||
|
int w = image.getWidth(); |
||||
|
int h = image.getHeight(); |
||||
|
int ySize = w * h; |
||||
|
int chromaWidth = (w + 1) / 2; |
||||
|
int chromaHeight = (h + 1) / 2; |
||||
|
int uvRowStrideOut = chromaWidth * 2; |
||||
|
int uvSize = uvRowStrideOut * chromaHeight; |
||||
|
byte[] out = new byte[ySize + uvSize]; |
||||
|
|
||||
|
Image.Plane yPlane = planes[0]; |
||||
|
ByteBuffer yBuffer = yPlane.getBuffer().duplicate(); |
||||
|
int yRowStride = yPlane.getRowStride(); |
||||
|
int yPixelStride = yPlane.getPixelStride(); |
||||
|
for (int row = 0; row < h; row++) { |
||||
|
int srcRow = row * yRowStride; |
||||
|
int dstRow = row * w; |
||||
|
for (int col = 0; col < w; col++) { |
||||
|
out[dstRow + col] = yBuffer.get(srcRow + col * yPixelStride); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Image.Plane uPlane = planes[1]; |
||||
|
Image.Plane vPlane = planes[2]; |
||||
|
ByteBuffer uBuffer = uPlane.getBuffer().duplicate(); |
||||
|
ByteBuffer vBuffer = vPlane.getBuffer().duplicate(); |
||||
|
int uRowStride = uPlane.getRowStride(); |
||||
|
int vRowStride = vPlane.getRowStride(); |
||||
|
int uPixelStride = uPlane.getPixelStride(); |
||||
|
int vPixelStride = vPlane.getPixelStride(); |
||||
|
|
||||
|
int uvOffset = ySize; |
||||
|
for (int row = 0; row < chromaHeight; row++) { |
||||
|
int uRow = row * uRowStride; |
||||
|
int vRow = row * vRowStride; |
||||
|
int dstRow = uvOffset + row * uvRowStrideOut; |
||||
|
for (int col = 0; col < chromaWidth; col++) { |
||||
|
int dst = dstRow + col * 2; |
||||
|
out[dst] = uBuffer.get(uRow + col * uPixelStride); |
||||
|
out[dst + 1] = vBuffer.get(vRow + col * vPixelStride); |
||||
|
} |
||||
|
} |
||||
|
return out; |
||||
|
} |
||||
|
|
||||
|
// Native callback to deliver decoded frames to native code |
||||
|
private static native void onFrameDecoded(int decoderId, byte[] data, int width, int height, long pts); |
||||
|
|
||||
|
// Called from native code to feed packet data to decoder |
||||
|
public static boolean decode(int decoderId, byte[] packet, long pts) { |
||||
|
MediaCodec codec = decoders.get(decoderId); |
||||
|
if (codec == null) return false; |
||||
|
try { |
||||
|
int inputIndex = codec.dequeueInputBuffer(10000); |
||||
|
if (inputIndex >= 0) { |
||||
|
ByteBuffer inputBuf = codec.getInputBuffer(inputIndex); |
||||
|
if (inputBuf == null) { |
||||
|
Log.w(TAG, "decode input buffer null"); |
||||
|
codec.queueInputBuffer(inputIndex, 0, 0, pts, 0); |
||||
|
return false; |
||||
|
} |
||||
|
inputBuf.clear(); |
||||
|
inputBuf.put(packet); |
||||
|
codec.queueInputBuffer(inputIndex, 0, packet.length, pts, 0); |
||||
|
} |
||||
|
return true; |
||||
|
} catch (Exception e) { |
||||
|
Log.w(TAG, "decode error: " + e); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void releaseDecoder(int decoderId) { |
||||
|
MediaCodec codec = decoders.remove(decoderId); |
||||
|
if (codec != null) { |
||||
|
try { codec.stop(); } catch (Throwable ignored) {} |
||||
|
try { codec.release(); } catch (Throwable ignored) {} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
#pragma once |
||||
|
|
||||
|
#include <optional> |
||||
|
#include <vector> |
||||
|
#include <cstdint> |
||||
|
|
||||
|
namespace FFmpeg::MediaCodecBridge { |
||||
|
|
||||
|
bool IsAvailable(); |
||||
|
|
||||
|
// Create a platform decoder for the given mime type ("video/avc", "video/x-vnd.on2.vp9", ...) |
||||
|
// Returns decoder id (>0) on success, or 0 on failure. |
||||
|
int CreateDecoder(const char* mime, int width, int height); |
||||
|
void DestroyDecoder(int id); |
||||
|
|
||||
|
// Feed an encoded packet to the decoder. Returns true if accepted. |
||||
|
bool SendPacket(int id, const uint8_t* data, size_t size, int64_t pts); |
||||
|
|
||||
|
// Pop a decoded NV12 frame. Returns std::nullopt if none available. On success, fills width,height,pts |
||||
|
std::optional<std::vector<uint8_t>> PopDecodedFrame(int id, int& width, int& height, int64_t& pts); |
||||
|
|
||||
|
} // namespace FFmpeg::MediaCodecBridge |
||||
@ -0,0 +1,114 @@ |
|||||
|
// Android-specific JNI bridge implementation
|
||||
|
#ifdef __ANDROID__
|
||||
|
|
||||
|
#include "mediacodec_bridge.h"
|
||||
|
#include <jni.h>
|
||||
|
#include <vector>
|
||||
|
#include <map>
|
||||
|
#include <memory>
|
||||
|
#include <mutex>
|
||||
|
#include <condition_variable>
|
||||
|
#include <optional>
|
||||
|
#include <cstdint>
|
||||
|
#include "common/android/id_cache.h"
|
||||
|
#include "common/logging/log.h"
|
||||
|
|
||||
|
namespace FFmpeg::MediaCodecBridge { |
||||
|
|
||||
|
static jclass g_native_media_codec_class = nullptr; |
||||
|
static jmethodID g_create_decoder = nullptr; |
||||
|
static jmethodID g_release_decoder = nullptr; |
||||
|
static jmethodID g_decode_method = nullptr; |
||||
|
|
||||
|
struct DecoderState { |
||||
|
int id; |
||||
|
std::mutex mtx; |
||||
|
std::vector<uint8_t> frame; |
||||
|
int width = 0; |
||||
|
int height = 0; |
||||
|
int64_t pts = 0; |
||||
|
bool has_frame = false; |
||||
|
}; |
||||
|
|
||||
|
static std::mutex s_global_mtx; |
||||
|
static std::map<int, std::shared_ptr<DecoderState>> s_decoders; |
||||
|
|
||||
|
extern "C" JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_media_NativeMediaCodec_onFrameDecoded( |
||||
|
JNIEnv* env, jclass, jint decoderId, jbyteArray data, jint width, jint height, jlong pts) { |
||||
|
std::lock_guard lock(s_global_mtx); |
||||
|
auto it = s_decoders.find(decoderId); |
||||
|
if (it == s_decoders.end()) return; |
||||
|
auto& st = it->second; |
||||
|
const jsize len = env->GetArrayLength(data); |
||||
|
st->frame.resize(len); |
||||
|
env->GetByteArrayRegion(data, 0, len, reinterpret_cast<jbyte*>(st->frame.data())); |
||||
|
st->width = width; |
||||
|
st->height = height; |
||||
|
st->pts = pts; |
||||
|
st->has_frame = true; |
||||
|
} |
||||
|
|
||||
|
bool IsAvailable() { |
||||
|
// We assume the bridge is available if the Java class can be found.
|
||||
|
auto env = Common::Android::GetEnvForThread(); |
||||
|
if (!env) return false; |
||||
|
if (!g_native_media_codec_class) { |
||||
|
jclass cls = env->FindClass("org/yuzu/yuzu_emu/media/NativeMediaCodec"); |
||||
|
if (!cls) return false; |
||||
|
g_native_media_codec_class = reinterpret_cast<jclass>(env->NewGlobalRef(cls)); |
||||
|
g_create_decoder = env->GetStaticMethodID(g_native_media_codec_class, "createDecoder", "(Ljava/lang/String;II)I"); |
||||
|
g_release_decoder = env->GetStaticMethodID(g_native_media_codec_class, "releaseDecoder", "(I)V"); |
||||
|
g_decode_method = env->GetStaticMethodID(g_native_media_codec_class, "decode", "(I[BJ)Z"); |
||||
|
} |
||||
|
return g_native_media_codec_class != nullptr; |
||||
|
} |
||||
|
|
||||
|
int CreateDecoder(const char* mime, int width, int height) { |
||||
|
auto env = Common::Android::GetEnvForThread(); |
||||
|
if (!env) return 0; |
||||
|
jstring jmime = env->NewStringUTF(mime); |
||||
|
const int id = env->CallStaticIntMethod(g_native_media_codec_class, g_create_decoder, jmime, width, height); |
||||
|
env->DeleteLocalRef(jmime); |
||||
|
if (id <= 0) return 0; |
||||
|
std::lock_guard lock(s_global_mtx); |
||||
|
auto st = std::make_shared<DecoderState>(); |
||||
|
st->id = id; |
||||
|
s_decoders[id] = st; |
||||
|
return id; |
||||
|
} |
||||
|
|
||||
|
void DestroyDecoder(int id) { |
||||
|
auto env = Common::Android::GetEnvForThread(); |
||||
|
if (!env) return; |
||||
|
env->CallStaticVoidMethod(g_native_media_codec_class, g_release_decoder, id); |
||||
|
std::lock_guard lock(s_global_mtx); |
||||
|
s_decoders.erase(id); |
||||
|
} |
||||
|
|
||||
|
bool SendPacket(int id, const uint8_t* data, size_t size, int64_t pts) { |
||||
|
auto env = Common::Android::GetEnvForThread(); |
||||
|
if (!env) return false; |
||||
|
std::lock_guard lock(s_global_mtx); |
||||
|
auto it = s_decoders.find(id); |
||||
|
if (it == s_decoders.end()) return false; |
||||
|
jbyteArray arr = env->NewByteArray(static_cast<jsize>(size)); |
||||
|
env->SetByteArrayRegion(arr, 0, static_cast<jsize>(size), reinterpret_cast<const jbyte*>(data)); |
||||
|
jboolean ok = env->CallStaticBooleanMethod(g_native_media_codec_class, g_decode_method, id, arr, static_cast<jlong>(pts)); |
||||
|
env->DeleteLocalRef(arr); |
||||
|
return ok; |
||||
|
} |
||||
|
|
||||
|
std::optional<std::vector<uint8_t>> PopDecodedFrame(int id, int& width, int& height, int64_t& pts) { |
||||
|
std::lock_guard lock(s_global_mtx); |
||||
|
auto it = s_decoders.find(id); |
||||
|
if (it == s_decoders.end()) return std::nullopt; |
||||
|
auto& st = it->second; |
||||
|
if (!st->has_frame) return std::nullopt; |
||||
|
st->has_frame = false; |
||||
|
width = st->width; |
||||
|
height = st->height; |
||||
|
pts = st->pts; |
||||
|
return st->frame; |
||||
|
} |
||||
|
|
||||
|
#endif // __ANDROID__
|
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue