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