全链路强制F32

This commit is contained in:
2026-05-14 01:51:14 +08:00
parent f3c50017e9
commit 599c876d03
9 changed files with 414 additions and 309 deletions
+34
View File
@@ -125,6 +125,24 @@ TexturePtr FFmpegDecoder::ProcessFrameIntoTexture(AVFramePtr f,
instance_.avstream(), nullptr),
VideoParams::kInterlaceNone, p.divider);
// For YUV formats, force the output texture to F32 RGBA for maximum precision
switch (f->format) {
case AV_PIX_FMT_YUV420P:
case AV_PIX_FMT_YUV422P:
case AV_PIX_FMT_YUV444P:
case AV_PIX_FMT_YUV420P10LE:
case AV_PIX_FMT_YUV422P10LE:
case AV_PIX_FMT_YUV444P10LE:
case AV_PIX_FMT_YUV420P12LE:
case AV_PIX_FMT_YUV422P12LE:
case AV_PIX_FMT_YUV444P12LE:
vp.set_format(PixelFormat::F32);
vp.set_channel_count(VideoParams::kRGBAChannelCount);
break;
default:
break;
}
// Create texture
TexturePtr tex = p.renderer->CreateTexture(vp);
@@ -246,6 +264,11 @@ TexturePtr FFmpegDecoder::ProcessFrameIntoTexture(AVFramePtr f,
tex->handleFrame(f);
tex->Upload(f->data[0], f->linesize[0] / vp.GetBytesPerPixel());
break;
case AV_PIX_FMT_RGBAF32:
// RGBA F32 can be uploaded directly to the texture
tex->handleFrame(f);
tex->Upload(f->data[0], f->linesize[0] / vp.GetBytesPerPixel());
break;
}
// Deinterlace if necessary
@@ -752,6 +775,9 @@ PixelFormat FFmpegDecoder::GetNativePixelFormat(AVPixelFormat pix_fmt)
case AV_PIX_FMT_RGB48:
case AV_PIX_FMT_RGBA64:
return PixelFormat::U16;
case AV_PIX_FMT_RGBF32:
case AV_PIX_FMT_RGBAF32:
return PixelFormat::F32;
default:
return PixelFormat::INVALID;
}
@@ -762,9 +788,11 @@ int FFmpegDecoder::GetNativeChannelCount(AVPixelFormat pix_fmt)
switch (pix_fmt) {
case AV_PIX_FMT_RGB24:
case AV_PIX_FMT_RGB48:
case AV_PIX_FMT_RGBF32:
return VideoParams::kRGBChannelCount;
case AV_PIX_FMT_RGBA:
case AV_PIX_FMT_RGBA64:
case AV_PIX_FMT_RGBAF32:
return VideoParams::kRGBAChannelCount;
default:
return 0;
@@ -807,6 +835,7 @@ bool FFmpegDecoder::IsPixelFormatGLSLCompatible(AVPixelFormat f)
case AV_PIX_FMT_YUV444P12LE:
case AV_PIX_FMT_RGBA:
case AV_PIX_FMT_RGBA64LE:
case AV_PIX_FMT_RGBAF32:
return true;
default:
return false;
@@ -856,6 +885,11 @@ AVFramePtr FFmpegDecoder::PreProcessFrame(AVFramePtr f,
static_cast<AVPixelFormat>(dest->format), p.maximum_format);
}
// swscale does not support RGBAF32 as output, fallback to RGBA64
if (dest->format == AV_PIX_FMT_RGBAF32) {
dest->format = AV_PIX_FMT_RGBA64;
}
int r = av_frame_get_buffer(dest.get(), 0);
if (r < 0) {
FFmpegError(r);
+11
View File
@@ -159,6 +159,17 @@ TexturePtr OIIODecoder::RetrieveVideoInternal(const RetrieveVideoParams &p)
}
}
// Force F32 output for all still images
if (vp.format() != PixelFormat::F32) {
FramePtr f32_frame = buffer_.convert(PixelFormat::F32);
if (f32_frame) {
VideoParams f32_vp = vp;
f32_vp.set_format(PixelFormat::F32);
return p.renderer->CreateTexture(f32_vp, f32_frame->data(),
f32_frame->linesize_pixels());
}
}
return p.renderer->CreateTexture(vp, buffer_.data(),
buffer_.linesize_pixels());
}
+7 -2
View File
@@ -28,7 +28,7 @@ AVPixelFormat
FFmpegUtils::GetCompatiblePixelFormat(const AVPixelFormat &pix_fmt,
PixelFormat maximum)
{
AVPixelFormat possible_pix_fmts[3];
AVPixelFormat possible_pix_fmts[4];
possible_pix_fmts[0] = AV_PIX_FMT_RGBA;
@@ -36,7 +36,12 @@ FFmpegUtils::GetCompatiblePixelFormat(const AVPixelFormat &pix_fmt,
possible_pix_fmts[1] = AV_PIX_FMT_NONE;
} else {
possible_pix_fmts[1] = AV_PIX_FMT_RGBA64;
possible_pix_fmts[2] = AV_PIX_FMT_NONE;
if (maximum == PixelFormat::F32) {
possible_pix_fmts[2] = AV_PIX_FMT_RGBAF32;
possible_pix_fmts[3] = AV_PIX_FMT_NONE;
} else {
possible_pix_fmts[2] = AV_PIX_FMT_NONE;
}
}
return avcodec_find_best_pix_fmt_of_list(possible_pix_fmts, pix_fmt, 1,
+1 -1
View File
@@ -249,7 +249,7 @@ void Config::SetDefaults()
SetEntryInternal(QStringLiteral("OnlinePixelFormat"), NodeValue::kInt,
PixelFormat::F32);
SetEntryInternal(QStringLiteral("OfflinePixelFormat"), NodeValue::kInt,
PixelFormat::F16);
PixelFormat::F32);
SetEntryInternal(QStringLiteral("MarkerColor"), NodeValue::kInt,
ColorCoding::kLime);
+4
View File
@@ -503,6 +503,10 @@ void NodeTraverser::ResolveJobs(NodeValue &val)
}
else if (plugin::PluginJob* plugin_job=dynamic_cast<plugin::PluginJob*>(base_job)) {
VideoParams tex_params = job_tex->params();
// Force internal working format (F32) for plugin processing,
// matching FootageJob/GenerateJob behavior.
tex_params.set_format(GetCacheVideoParams().format());
tex_params.set_channel_count(VideoParams::kRGBAChannelCount);
TexturePtr tex = CreateTexture(tex_params);
-8
View File
@@ -443,14 +443,6 @@ olive::plugin::OliveClipInstance::getImage(OfxTime time,
return image;
}
// If this input clip was never populated (no setInputTexture call),
// return nullptr so the plugin knows no image is available.
// This prevents EXC_BAD_ACCESS when plugins (e.g. ofxsMaskMixPix)
// try to read pixel data from an empty/invalid image buffer.
if (!getConnected()) {
return nullptr;
}
// Fetch on demand for the input clip.
// Use plugin-preferred params to ensure the image format matches
// what the plugin expects (may differ from input texture format)
+233 -297
View File
@@ -489,9 +489,12 @@ static void ChooseSupportedOutputParams(
}
}
static olive::TexturePtr ConvertTextureForParams(
olive::TexturePtr src,
const olive::VideoParams &dst_params);
// Forward declarations for functions defined later in this file.
static olive::AVFramePtr ConvertFrameIfNeeded(olive::AVFramePtr src,
const olive::VideoParams &dst_params,
olive::Renderer *renderer);
static olive::TexturePtr ConvertTextureForParams(olive::TexturePtr src,
const olive::VideoParams &dst_params);
// 作用:根据插件能力与偏好选择输入格式并执行转换。
// Purpose: Select a supported input format and convert texture for the clip.
@@ -705,14 +708,13 @@ static olive::AVFramePtr create_avframe_from_ofx_image(OFX::Host::ImageEffect::I
// 前置声明:必要时将 AVFrame 转换为目标 VideoParams 对应格式。
// Forward declaration: Convert AVFrame to match destination VideoParams when needed.
static olive::AVFramePtr ConvertFrameIfNeeded(olive::AVFramePtr src,
const olive::VideoParams &dst_params);
// 作用:按指定 VideoParams 复制 OFX Image 到 AVFrame,必要时做格式转换。
// Purpose: Copy OFX Image data into an AVFrame using target VideoParams with format conversion.
static olive::AVFramePtr create_avframe_from_ofx_image_with_params(
OFX::Host::ImageEffect::Image &image,
const olive::VideoParams &params)
const olive::VideoParams &params,
olive::Renderer *renderer = nullptr)
{
void *data_ptr = image.getPointerProperty(kOfxImagePropData);
if (!data_ptr) {
@@ -730,19 +732,21 @@ static olive::AVFramePtr create_avframe_from_ofx_image_with_params(
// Get ACTUAL source format from image properties
std::string image_depth = image.getStringProperty(kOfxImageEffectPropPixelDepth);
std::string image_comp = image.getStringProperty(kOfxImageEffectPropComponents);
int src_channel_count = 4;
if (image_comp == kOfxImageComponentRGB) src_channel_count = 3;
else if (image_comp == kOfxImageComponentAlpha) src_channel_count = 1;
// NOTE: FP16 (Half) format handling has been removed.
// FP16 data is now treated as U16 (2 bytes per component) and converted via FFmpeg.
int src_bytes_per_component = 1;
if (image_depth == kOfxBitDepthShort) src_bytes_per_component = 2;
else if (image_depth == kOfxBitDepthHalf) src_bytes_per_component = 2;
else if (image_depth == kOfxBitDepthHalf) src_bytes_per_component = 2; // Treat as U16
else if (image_depth == kOfxBitDepthFloat) src_bytes_per_component = 4;
const int src_bytes_per_pixel = src_channel_count * src_bytes_per_component;
const int dst_bytes_per_pixel = params.channel_count() * params.format().byte_count();
int row_bytes = image.getIntProperty(kOfxImagePropRowBytes);
if (row_bytes <= 0) {
row_bytes = width * src_bytes_per_pixel;
@@ -752,12 +756,13 @@ static olive::AVFramePtr create_avframe_from_ofx_image_with_params(
src += bounds[1] * row_bytes + bounds[0] * src_bytes_per_pixel;
// Check if format conversion is needed
bool needs_conversion = (src_bytes_per_pixel != dst_bytes_per_pixel) ||
bool needs_conversion = (src_bytes_per_pixel != dst_bytes_per_pixel) ||
(src_channel_count != params.channel_count());
if (needs_conversion) {
// Create source frame with actual format
AVPixelFormat src_fmt = AV_PIX_FMT_NONE;
if (src_channel_count == 4) {
if (src_bytes_per_component == 1) src_fmt = AV_PIX_FMT_RGBA;
else if (src_bytes_per_component == 2) src_fmt = AV_PIX_FMT_RGBA64LE;
@@ -771,21 +776,29 @@ static olive::AVFramePtr create_avframe_from_ofx_image_with_params(
else if (src_bytes_per_component == 2) src_fmt = AV_PIX_FMT_GRAY16LE;
else if (src_bytes_per_component == 4) src_fmt = AV_PIX_FMT_GRAYF32LE;
}
if (src_fmt != AV_PIX_FMT_NONE) {
olive::AVFramePtr src_frame = olive::CreateAVFramePtr();
src_frame->width = width;
src_frame->height = height;
src_frame->format = src_fmt;
if (av_frame_get_buffer(src_frame.get(), 0) >= 0) {
// Copy source data row by row
for (int y = 0; y < height; ++y) {
memcpy(src_frame->data[0] + y * src_frame->linesize[0],
src + y * row_bytes, width * src_bytes_per_pixel);
}
// Convert to destination format
return ConvertFrameIfNeeded(src_frame, params);
return ConvertFrameIfNeeded(src_frame, params, renderer);
} else {
qWarning().noquote() << "[WARN] av_frame_get_buffer failed for src_fmt=" << src_fmt;
}
} else {
qWarning().noquote()
<< "[WARN] src_fmt is NONE for depth=" << QString::fromStdString(image_depth);
}
}
@@ -945,10 +958,45 @@ int olive::plugin::detail::BytesToPixels(int byte_linesize,
return byte_linesize / bytes_per_pixel;
}
// 作用:将 AVPixelFormat 映射为 Olive 的 PixelFormat 和通道数(仅常见 packed 格式)。
static void GetOliveFormatFromAV(AVPixelFormat fmt, olive::core::PixelFormat *out_fmt, int *out_ch)
{
switch (fmt) {
case AV_PIX_FMT_GRAY8:
*out_fmt = olive::core::PixelFormat::U8; *out_ch = 1; return;
case AV_PIX_FMT_RGB24:
*out_fmt = olive::core::PixelFormat::U8; *out_ch = 3; return;
case AV_PIX_FMT_RGBA:
*out_fmt = olive::core::PixelFormat::U8; *out_ch = 4; return;
case AV_PIX_FMT_GRAY16LE:
case AV_PIX_FMT_GRAY16BE:
*out_fmt = olive::core::PixelFormat::U16; *out_ch = 1; return;
case AV_PIX_FMT_RGB48LE:
case AV_PIX_FMT_RGB48BE:
*out_fmt = olive::core::PixelFormat::U16; *out_ch = 3; return;
case AV_PIX_FMT_RGBA64LE:
case AV_PIX_FMT_RGBA64BE:
*out_fmt = olive::core::PixelFormat::U16; *out_ch = 4; return;
case AV_PIX_FMT_GRAYF32LE:
case AV_PIX_FMT_GRAYF32BE:
*out_fmt = olive::core::PixelFormat::F32; *out_ch = 1; return;
case AV_PIX_FMT_RGBF32LE:
case AV_PIX_FMT_RGBF32BE:
*out_fmt = olive::core::PixelFormat::F32; *out_ch = 3; return;
case AV_PIX_FMT_RGBAF32LE:
case AV_PIX_FMT_RGBAF32BE:
*out_fmt = olive::core::PixelFormat::F32; *out_ch = 4; return;
default:
*out_fmt = olive::core::PixelFormat::INVALID; *out_ch = 0; return;
}
}
// 作用:必要时将 AVFrame 转换为目标 VideoParams 对应格式。
// Purpose: Convert AVFrame to match destination VideoParams when needed.
// 优先使用 FFmpeg sws_scale;若不支持且 renderer 可用,则走 GPU 路径。
// 删除所有手写 CPU 像素循环,避免精度损失与性能瓶颈。
static olive::AVFramePtr ConvertFrameIfNeeded(olive::AVFramePtr src,
const olive::VideoParams &dst_params)
const olive::VideoParams &dst_params,
olive::Renderer *renderer = nullptr)
{
if (!src) {
return nullptr;
@@ -959,6 +1007,7 @@ static olive::AVFramePtr ConvertFrameIfNeeded(olive::AVFramePtr src,
return src;
}
// Same format & size, no conversion needed
if (src->format == dst_fmt &&
src->width == dst_params.width() &&
src->height == dst_params.height()) {
@@ -970,256 +1019,61 @@ static olive::AVFramePtr ConvertFrameIfNeeded(olive::AVFramePtr src,
dst->width = dst_params.width();
dst->height = dst_params.height();
if (av_frame_get_buffer(dst.get(), 0) < 0) {
qWarning().noquote() << "[WARN] av_frame_get_buffer failed for dst_fmt=" << dst_fmt;
return src;
}
auto float_channels = [](AVPixelFormat fmt) -> int {
switch (fmt) {
case AV_PIX_FMT_GRAYF32LE:
case AV_PIX_FMT_GRAYF32BE:
return 1;
case AV_PIX_FMT_RGBF32LE:
case AV_PIX_FMT_RGBF32BE:
return 3;
case AV_PIX_FMT_RGBAF32LE:
case AV_PIX_FMT_RGBAF32BE:
return 4;
default:
return 0;
}
};
auto dst_packed_info = [](AVPixelFormat fmt, int *channels,
int *bytes_per_component) -> bool {
switch (fmt) {
case AV_PIX_FMT_GRAY8:
*channels = 1;
*bytes_per_component = 1;
return true;
case AV_PIX_FMT_RGB24:
*channels = 3;
*bytes_per_component = 1;
return true;
case AV_PIX_FMT_RGBA:
*channels = 4;
*bytes_per_component = 1;
return true;
case AV_PIX_FMT_GRAY16LE:
*channels = 1;
*bytes_per_component = 2;
return true;
case AV_PIX_FMT_RGB48LE:
*channels = 3;
*bytes_per_component = 2;
return true;
case AV_PIX_FMT_RGBA64LE:
*channels = 4;
*bytes_per_component = 2;
return true;
default:
return false;
}
};
auto float_dst_from_packed = [&](const olive::AVFramePtr &packed_src,
AVPixelFormat float_fmt,
const olive::AVFramePtr &float_dst) -> bool {
if (!packed_src || !float_dst || !packed_src->data[0] ||
!float_dst->data[0]) {
return false;
}
const int dst_channels = float_channels(float_fmt);
if (dst_channels == 0) {
return false;
}
int src_channels = 0;
int bytes_per_component = 0;
if (!dst_packed_info(static_cast<AVPixelFormat>(packed_src->format),
&src_channels, &bytes_per_component)) {
return false;
}
const float inv_scale =
(bytes_per_component == 2) ? (1.0f / 65535.0f)
: (1.0f / 255.0f);
for (int y = 0; y < packed_src->height; ++y) {
const uint8_t *src_row =
packed_src->data[0] + y * packed_src->linesize[0];
float *dst_row = reinterpret_cast<float *>(
float_dst->data[0] + y * float_dst->linesize[0]);
if (bytes_per_component == 2) {
const uint16_t *src_u16 =
reinterpret_cast<const uint16_t *>(src_row);
for (int x = 0; x < packed_src->width; ++x) {
const uint16_t *pix = src_u16 + x * src_channels;
float r = pix[0] * inv_scale;
float g = (src_channels > 1) ? pix[1] * inv_scale : r;
float b = (src_channels > 2) ? pix[2] * inv_scale : r;
float a = (src_channels > 3) ? pix[3] * inv_scale : 1.0f;
dst_row[x * dst_channels + 0] = r;
if (dst_channels > 1) {
dst_row[x * dst_channels + 1] = g;
}
if (dst_channels > 2) {
dst_row[x * dst_channels + 2] = b;
}
if (dst_channels > 3) {
dst_row[x * dst_channels + 3] = a;
}
}
} else {
for (int x = 0; x < packed_src->width; ++x) {
const uint8_t *pix = src_row + x * src_channels;
float r = pix[0] * inv_scale;
float g = (src_channels > 1) ? pix[1] * inv_scale : r;
float b = (src_channels > 2) ? pix[2] * inv_scale : r;
float a = (src_channels > 3) ? pix[3] * inv_scale : 1.0f;
dst_row[x * dst_channels + 0] = r;
if (dst_channels > 1) {
dst_row[x * dst_channels + 1] = g;
}
if (dst_channels > 2) {
dst_row[x * dst_channels + 2] = b;
}
if (dst_channels > 3) {
dst_row[x * dst_channels + 3] = a;
}
}
}
}
return true;
};
const int dst_float_channels = float_channels(dst_fmt);
if (dst_float_channels > 0) {
int src_channels = 0;
int bytes_per_component = 0;
if (dst_packed_info(static_cast<AVPixelFormat>(src->format),
&src_channels, &bytes_per_component)) {
if (float_dst_from_packed(src, dst_fmt, dst)) {
return dst;
}
} else {
olive::AVFramePtr packed = olive::CreateAVFramePtr();
AVPixelFormat packed_fmt = (dst_float_channels == 4)
? AV_PIX_FMT_RGBA
: (dst_float_channels == 3)
? AV_PIX_FMT_RGB24
: AV_PIX_FMT_GRAY8;
packed->format = packed_fmt;
packed->width = dst->width;
packed->height = dst->height;
if (av_frame_get_buffer(packed.get(), 0) >= 0) {
SwsContext *pre_ctx = sws_getContext(
src->width, src->height,
static_cast<AVPixelFormat>(src->format),
packed->width, packed->height, packed_fmt, SWS_POINT,
nullptr, nullptr, nullptr);
if (pre_ctx) {
sws_scale(pre_ctx, src->data, src->linesize, 0, src->height,
packed->data, packed->linesize);
sws_freeContext(pre_ctx);
if (float_dst_from_packed(packed, dst_fmt, dst)) {
return dst;
}
}
}
}
}
auto clamp01 = [](float v) -> float {
return std::clamp(v, 0.0f, 1.0f);
};
const int src_float_channels = float_channels(
static_cast<AVPixelFormat>(src->format));
if (src_float_channels > 0) {
int dst_channels = 0;
int bytes_per_component = 0;
if (dst_packed_info(dst_fmt, &dst_channels, &bytes_per_component) &&
src->data[0] && dst->data[0]) {
for (int y = 0; y < src->height; ++y) {
const float *src_row = reinterpret_cast<const float *>(
src->data[0] + y * src->linesize[0]);
uint8_t *dst_row = dst->data[0] + y * dst->linesize[0];
if (bytes_per_component == 2) {
auto *dst_row_u16 =
reinterpret_cast<uint16_t *>(dst_row);
for (int x = 0; x < src->width; ++x) {
const float *pix =
src_row + x * src_float_channels;
float r = pix[0];
float g = (src_float_channels > 1) ? pix[1] : r;
float b = (src_float_channels > 2) ? pix[2] : r;
float a = (src_float_channels > 3) ? pix[3] : 1.0f;
if (dst_channels == 1) {
float luma = 0.2126f * r + 0.7152f * g + 0.0722f * b;
dst_row_u16[x] = static_cast<uint16_t>(
std::lround(clamp01(luma) * 65535.0f));
continue;
}
dst_row_u16[x * dst_channels + 0] =
static_cast<uint16_t>(
std::lround(clamp01(r) * 65535.0f));
dst_row_u16[x * dst_channels + 1] =
static_cast<uint16_t>(
std::lround(clamp01(g) * 65535.0f));
dst_row_u16[x * dst_channels + 2] =
static_cast<uint16_t>(
std::lround(clamp01(b) * 65535.0f));
if (dst_channels == 4) {
dst_row_u16[x * dst_channels + 3] =
static_cast<uint16_t>(
std::lround(clamp01(a) * 65535.0f));
}
}
} else {
for (int x = 0; x < src->width; ++x) {
const float *pix =
src_row + x * src_float_channels;
float r = pix[0];
float g = (src_float_channels > 1) ? pix[1] : r;
float b = (src_float_channels > 2) ? pix[2] : r;
float a = (src_float_channels > 3) ? pix[3] : 1.0f;
if (dst_channels == 1) {
float luma = 0.2126f * r + 0.7152f * g + 0.0722f * b;
dst_row[x] = static_cast<uint8_t>(
std::lround(clamp01(luma) * 255.0f));
continue;
}
dst_row[x * dst_channels + 0] =
static_cast<uint8_t>(
std::lround(clamp01(r) * 255.0f));
dst_row[x * dst_channels + 1] =
static_cast<uint8_t>(
std::lround(clamp01(g) * 255.0f));
dst_row[x * dst_channels + 2] =
static_cast<uint8_t>(
std::lround(clamp01(b) * 255.0f));
if (dst_channels == 4) {
dst_row[x * dst_channels + 3] =
static_cast<uint8_t>(
std::lround(clamp01(a) * 255.0f));
}
}
}
}
return dst;
}
}
// Try FFmpeg sws_scale first
SwsContext *sws_ctx = sws_getContext(
src->width, src->height, static_cast<AVPixelFormat>(src->format),
dst->width, dst->height, dst_fmt, SWS_POINT,
nullptr, nullptr, nullptr);
if (!sws_ctx) {
return src;
if (sws_ctx) {
int ret = sws_scale(sws_ctx, src->data, src->linesize, 0, src->height,
dst->data, dst->linesize);
sws_freeContext(sws_ctx);
if (ret > 0) {
return dst;
}
}
sws_scale(sws_ctx, src->data, src->linesize, 0, src->height,
dst->data, dst->linesize);
sws_freeContext(sws_ctx);
// sws_scale failed (e.g. RGBAF32 not supported). Use GPU if renderer available.
if (renderer && src->data[0]) {
olive::core::PixelFormat src_fmt;
int src_ch;
GetOliveFormatFromAV(static_cast<AVPixelFormat>(src->format), &src_fmt, &src_ch);
if (src_fmt != olive::core::PixelFormat::INVALID && src_ch > 0) {
olive::VideoParams src_vp(src->width, src->height, src_fmt, src_ch);
int src_bpp = olive::VideoParams::GetBytesPerPixel(src_fmt, src_ch);
int src_linesize_pixels = (src_bpp > 0) ? src->linesize[0] / src_bpp : src->width;
return dst;
olive::TexturePtr src_tex = renderer->CreateTexture(
src_vp, src->data[0], src_linesize_pixels);
if (src_tex) {
olive::TexturePtr dst_tex = renderer->CreateTexture(dst_params);
if (dst_tex) {
olive::ShaderJob job;
job.Insert(QStringLiteral("ove_maintex"),
olive::NodeValue(olive::NodeValue::kTexture,
QVariant::fromValue(src_tex)));
renderer->BlitToTexture(renderer->GetDefaultShader(), job,
dst_tex.get(), false);
// Download result back to AVFrame
int dst_bpp = dst_params.GetBytesPerPixel();
int dst_linesize_pixels = (dst_bpp > 0) ? dst->linesize[0] / dst_bpp : dst->width;
dst_tex->Download(dst->data[0], dst_linesize_pixels);
return dst;
}
}
}
}
qWarning().noquote()
<< "[WARN] ConvertFrameIfNeeded failed (sws_scale + GPU both unavailable). "
<< "Returning unconverted source. src_fmt=" << src->format
<< " dst_fmt=" << dst_fmt;
return src;
}
// 作用:从字节行跨度换算像素行跨度。
@@ -1234,8 +1088,8 @@ static int LinesizeToPixels(const olive::VideoParams &params, int linesize_bytes
return linesize_bytes / bytes_per_pixel;
}
// 作用:将纹理转换为指定 VideoParamsCPU 路径,必要时回读)。
// Purpose: Convert texture to target VideoParams (CPU path with readback).
// 作用:将纹理转换为指定 VideoParams。优先使用 GPU shader 做格式转换,
// 避免 CPU 回读/转换/上传的性能损失和精度损失。
static olive::TexturePtr ConvertTextureForParams(olive::TexturePtr src,
const olive::VideoParams &dst_params)
{
@@ -1250,6 +1104,23 @@ static olive::TexturePtr ConvertTextureForParams(olive::TexturePtr src,
return src;
}
// GPU path: blit directly to destination format using default shader.
// OpenGL texture sampling automatically normalizes U8/U16 to float,
// and write-out quantizes float back to U8/U16 when needed.
if (auto *renderer = src->renderer()) {
olive::TexturePtr dst = renderer->CreateTexture(dst_params);
if (dst) {
olive::ShaderJob job;
job.Insert(QStringLiteral("ove_maintex"),
olive::NodeValue(olive::NodeValue::kTexture,
QVariant::fromValue(src)));
renderer->BlitToTexture(renderer->GetDefaultShader(), job, dst.get(),
false);
return dst;
}
}
// CPU fallback: readback, sws_scale, re-upload
olive::AVFramePtr frame = src->frame();
if (!frame || !frame->data[0]) {
frame = ReadbackTextureToFrame(src, src_params);
@@ -1258,7 +1129,7 @@ static olive::TexturePtr ConvertTextureForParams(olive::TexturePtr src,
return nullptr;
}
olive::AVFramePtr converted = ConvertFrameIfNeeded(frame, dst_params);
olive::AVFramePtr converted = ConvertFrameIfNeeded(frame, dst_params, nullptr);
if (!converted || !converted->data[0]) {
return nullptr;
}
@@ -1290,6 +1161,7 @@ static olive::TexturePtr ConvertTextureForParams(olive::TexturePtr src,
return dst;
}
// 作用:安全获取插件标识符,便于日志输出。
// Purpose: Safely fetch plugin identifier for logging.
static QString PluginIdForInstance(const OFX::Host::ImageEffect::Instance *instance)
@@ -1387,7 +1259,72 @@ static olive::AVFramePtr DownloadTextureToFrame(const olive::TexturePtr &tex)
const olive::VideoParams &params = tex->params();
return ReadbackTextureToFrame(tex, params);
}
inline std::vector<std::string> GetPluginSupportedDepths(const OFX::Host::ImageEffect::Descriptor& desc)
{
std::vector<std::string> depths;
const OFX::Host::Property::Set& props = desc.getProps();
// 获取数组维度(支持几种深度)
int dim = props.getDimension(kOfxImageEffectPropSupportedPixelDepths);
for (int i = 0; i < dim; ++i) {
// Host Support Library 返回 const std::string&
const std::string& val = props.getStringProperty(
kOfxImageEffectPropSupportedPixelDepths,
i
);
if (!val.empty() && val != kOfxBitDepthNone) {
depths.push_back(val);
}
}
return depths;
}
// 查询插件/宿主是否支持「各 clip 不同深度」
inline bool SupportsMultipleClipDepths(const OFX::Host::ImageEffect::Descriptor& desc)
{
const OFX::Host::Property::Set& props = desc.getProps();
// 这是单值 int 属性(0 或 1),n = 0
int val = props.getIntProperty(kOfxImageEffectPropSupportsMultipleClipDepths, 0);
return val != 0;
}
// 作用:根据插件描述符声明的 kOfxImageEffectPropSupportedPixelDepths
// 按优先级 F32 > U16 > U8 > F16 选择最佳输入像素格式。
// 参考 OpenFX API: OfxImageEffectPropSupportedPixelDepths
// Purpose: Select best input pixel format from plugin descriptor's supported
// depth list. Priority: F32 > U16 > U8 > F16.
static PixelFormat SelectBestPluginInputFormat(
const OFX::Host::ImageEffect::Descriptor& desc)
{
const OFX::Host::Property::Set& props = desc.getProps();
int dim = props.getDimension(kOfxImageEffectPropSupportedPixelDepths);
bool supports_f32 = false;
bool supports_u16 = false;
bool supports_u8 = false;
bool supports_f16 = false;
for (int i = 0; i < dim; ++i) {
const std::string& depth = props.getStringProperty(
kOfxImageEffectPropSupportedPixelDepths, i);
if (depth == kOfxBitDepthFloat) {
supports_f32 = true;
} else if (depth == kOfxBitDepthShort) {
supports_u16 = true;
} else if (depth == kOfxBitDepthByte) {
supports_u8 = true;
} else if (depth == kOfxBitDepthHalf) {
supports_f16 = true;
}
}
// 优先级:F32 > U16 > U8 > F16
if (supports_f32) return PixelFormat::F32;
if (supports_u16) return PixelFormat::U16;
if (supports_u8) return PixelFormat::U8;
if (supports_f16) return PixelFormat::F16;
return PixelFormat::INVALID;
}
// 作用:执行 OFX 插件渲染全流程(准备输入、调用动作、处理输出)。
// Purpose: Run full OFX plugin render flow (inputs, actions, outputs).
void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin::PluginJob& job,
@@ -1494,7 +1431,7 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin::
input_clips[entry.first] = input_clip;
}
}
// call getClipPreferences to know which format plugin requires
OFX::Host::Property::Set args;
args.setDoubleProperty(kOfxPropTime, frame);
@@ -1530,9 +1467,11 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin::
// effect needs to render a given frame (clipped to the RoD).
//
// In our example we are doing full frame fetches regardless.
// set correct format for input
auto &descriptor = instance->getDescriptor();
for (const auto &entry : input_clips) {
if (entry.first == kOfxImageEffectOutputClipName) {
continue;
@@ -1549,27 +1488,23 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin::
input_tex->handleFrame(ptr);
}
if (is_usable_input(input_tex)) {
// Determine the plugin's preferred format for this clip
std::string bitdepth = input_clip->getUnmappedBitDepth();
std::string component = input_clip->getUnmappedComponents();
// Query the plugin descriptor for supported pixel depths and pick
// the best one according to our priority: F32 > U16 > U8 > F16.
// OpenFX reference: kOfxImageEffectPropSupportedPixelDepths on
// the image effect descriptor lists all depths the plugin can handle.
PixelFormat chosen_fmt = SelectBestPluginInputFormat(descriptor);
VideoParams params = input_tex->params();
PixelFormat plugin_format = PixelFormat::from_ofx(bitdepth);
if (plugin_format != PixelFormat::INVALID) {
params.set_format(plugin_format);
if (chosen_fmt != PixelFormat::INVALID && params.format() != chosen_fmt) {
params.set_format(chosen_fmt);
TexturePtr converted_tex =
ConvertTextureForParams(input_tex, params);
if (converted_tex && is_usable_input(converted_tex)) {
input_tex = converted_tex;
input_textures[entry.first] = input_tex;
}
}
if (!component.empty() && component != kOfxImageComponentNone) {
params.set_channel_count(component);
}
// Convert the texture to the plugin's preferred format BEFORE
// setting it on the clip, so the OFX Image receives correctly
// formatted pixel data.
TexturePtr converted_tex =
ConvertTextureForParams(input_tex, params);
if (converted_tex && is_usable_input(converted_tex)) {
input_tex = converted_tex;
}
input_textures[entry.first] = input_tex;
// Now set the (possibly converted) texture on the clip
input_clip->setInputTexture(input_tex, frame);
OfxRectD rod;
rod.x1 = 0;
@@ -1589,21 +1524,22 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin::
return;
}
// set correct format for output
VideoParams output_params = destination_params; // params for plugin
// First set the destination params on the clip
output_clip->setParams(output_params);
// Get the plugin's preferred bitdepth/component from the instance (not descriptor)
// Use getUnmappedBitDepth/Components which use the instance's params_
std::string bitdepth = output_clip->getUnmappedBitDepth();
std::string component = output_clip->getUnmappedComponents();
// If plugin returns a valid format different from destination, update output_params
PixelFormat plugin_format = PixelFormat::from_ofx(bitdepth);
if (plugin_format != PixelFormat::INVALID) {
output_params.set_format(plugin_format);
// Query plugin-supported depths and pick best according to our priority:
// F32 > U16 > U8 > F16. If plugin supports F32 we render directly to F32
// (zero conversion). If plugin only supports U8/U16 we let it render in
// that format and ConvertFrameIfNeeded will convert back to F32 afterwards.
VideoParams output_params = destination_params;
PixelFormat best_fmt = SelectBestPluginInputFormat(descriptor);
if (best_fmt != PixelFormat::INVALID) {
output_params.set_format(best_fmt);
}
output_clip->setParams(output_params);
std::string component = output_clip->getUnmappedComponents();
if (!component.empty() && component != kOfxImageComponentNone) {
output_params.set_channel_count(component);
}
// Re-set params so the clip knows the possibly-changed channel count
output_clip->setParams(output_params);
// The render window is in pixel coordinates
// ie: render scale and a PAR of not 1
@@ -1612,7 +1548,7 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin::
renderWindow.x2 = destination_params.width();
renderWindow.y2 = destination_params.height();
stat = instance->beginRenderAction(frame, numFramesToRender,
1.0, false, renderScale, true,
interactive);
@@ -1628,7 +1564,7 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin::
AttachOutputTexture(destination);
}
#endif
if (!output_params.is_valid()) {
qWarning().noquote()
@@ -1695,7 +1631,7 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin::
}
if (!use_opengl) {
AVFramePtr frame_ptr =
AVFramePtr frame_ptr =
create_avframe_from_ofx_image_with_params(*output_image,
output_params);
if (!frame_ptr) {
@@ -1706,7 +1642,7 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin::
renderScale, true, interactive);
return;
}
AVFramePtr converted = ConvertFrameIfNeeded(frame_ptr, destination_params);
AVFramePtr converted = ConvertFrameIfNeeded(frame_ptr, destination_params, this);
const AVPixelFormat expected_fmt =
GetDestinationAVPixelFormat(destination_params);
destination->handleFrame(converted);
@@ -1733,7 +1669,7 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin::
#endif
if (frame_ptr && destination) {
AVFramePtr converted =
ConvertFrameIfNeeded(frame_ptr, destination_params);
ConvertFrameIfNeeded(frame_ptr, destination_params, this);
const AVPixelFormat expected_fmt =
GetDestinationAVPixelFormat(destination_params);
destination->handleFrame(converted);
+2 -1
View File
@@ -165,7 +165,8 @@ void RenderProcessor::Run()
SetCancelPointer(ticket_->GetCancelAtom());
VideoParams params=ticket_->property("vparam").value<VideoParams>();
SetCacheVideoParams(ticket_->property("vparam").value<VideoParams>());
params.set_format(PixelFormat::F32);
SetCacheVideoParams(params);
SetCacheAudioParams(ticket_->property("aparam").value<AudioParams>());
if (IsCancelled()) {
+122
View File
@@ -0,0 +1,122 @@
# 素材读入强制转换为 RGBAF32 并内部全链路使用 F32 处理 — 实施计划
## 1. 现状分析
### 1.1 视频读入位置
视频/图像素材在以下位置被读入并解码为 GPU Texture
| 层级 | 文件 | 职责 |
|------|------|------|
| 解码接口 | `app/codec/decoder.h` / `.cpp` | 基类 `Decoder`,定义 `RetrieveVideo(RetrieveVideoParams)` 公共接口 |
| FFmpeg 解码 | `app/codec/ffmpeg/ffmpegdecoder.cpp` | `FFmpegDecoder::RetrieveVideoInternal()` —— 核心视频解码路径 |
| OIIO 解码 | `app/codec/oiio/oiiodecoder.cpp` | `OIIODecoder::RetrieveVideoInternal()` —— 静态图片解码路径 |
| 渲染触发 | `app/render/renderprocessor.cpp` | `ProcessVideoFootage()` —— 在节点图遍历中触发解码,并做颜色管理转换 |
| 遍历调度 | `app/node/traverser.cpp` | `ResolveJobs()` —— 将 `FootageJob` 分发给 `ProcessVideoFootage()` |
**数据流:**
```
文件 → FFmpegDecoder::RetrieveVideoInternal()
→ RetrieveFrame() 解码出 AVFrame
→ PreProcessFrame() CPU 缩放/格式转换 (sws_scale_frame)
→ ProcessFrameIntoTexture() 上传为 GPU Texture
→ YUV 格式:上传为 3 个 plane texture + YUV→RGB shader
→ RGBA/RGBA64LE:直接 glTexSubImage2D 上传
→ RenderProcessor::ProcessVideoFootage()
→ BlitColorManaged() OCIO 颜色空间转换 shader
→ 进入节点图后续处理
```
### 1.2 像素格式体系
- **核心枚举:** `ext/core/include/olive/core/render/pixelformat.h` 定义 `PixelFormat::U8 / U16 / F16 / F32`
- **GPU 格式映射:** `app/render/opengl/openglrenderer.cpp` 已将 `F32 + 4ch` 映射到 `GL_RGBA32F / GL_FLOAT`
- **内部工作格式:** `NodeTraverser::GetCacheVideoParams().format()` 决定节点图内部缓存格式
- **项目默认配置:** `app/config/config.cpp``OnlinePixelFormat = F32``OfflinePixelFormat = F16`,说明设计意图就是在线编辑使用 F32
### 1.3 当前 F32 支持的关键缺失
1. **`FFmpegDecoder::GetNativePixelFormat()` 不识别 F32 FFmpeg 格式**
- 仅映射 `RGBA → U8``RGBA64 → U16`
- `AV_PIX_FMT_RGBAF32``AV_PIX_FMT_RGBF32` 等落入 `default: INVALID`
2. **`IsPixelFormatGLSLCompatible()` 未将 RGBAF32 列为 GLSL 兼容**
- 这会导致即使解码器输出 RGBAF32,也会强制走 `sws_scale_frame` CPU 转换路径
3. **`ProcessFrameIntoTexture()` 直接上传路径缺少 RGBAF32 分支**
- 当前只有 `YUV...``RGBA / RGBA64LE` 两个直接上传分支,没有 `RGBAF32` 等直接上传路径
4. **`PreProcessFrame()``sws_scale_frame` 目标格式选择需验证 F32 支持**
- `FFmpegUtils::GetCompatiblePixelFormat(..., maximum=F32)` 理论上应返回 `AV_PIX_FMT_RGBAF32`,但需实测验证
5. **OIIO 解码器已原生支持 F32FLOAT → F32),无需修改**
## 2. 目标
- **读入时转换:** 无论源素材格式(YUV、U8、U16、F16 等),在解码器层面统一转换为 **RGBAF32** 后上传 GPU
- **内部全链路 F32:** 节点图遍历、效果处理、合成、缓存等内部环节全部使用 `PixelFormat::F32`4 通道)
- **导出保持灵活:** 导出/编码时从 F32 转换为目标格式,保持现有编码逻辑
## 3. 实施方案:全局强制 F32
**思路:** 将 F32 作为唯一的内部工作格式,在解码器出口强制转换。
**改动点:**
1. **解码器层强制 F32 输出**
- `FFmpegDecoder::RetrieveVideoInternal()`
- 修改 `RetrieveVideoParams` 或内部逻辑,令 `maximum_format` 固定为 `F32`
-`PreProcessFrame()` 中,若源格式非 RGBAF32,通过 `sws_scale_frame` 转换到 `AV_PIX_FMT_RGBAF32`
-`ProcessFrameIntoTexture()` 中增加 `AV_PIX_FMT_RGBAF32` 直接上传分支(`GL_RGBA32F / GL_FLOAT`
- `OIIODecoder::RetrieveVideoInternal()`
- OIIO 读入后,若格式非 F32,通过 `Frame::convert(PixelFormat::F32)` 转换,再上传
2. **修复 F32 格式映射**
- `FFmpegDecoder::GetNativePixelFormat()` 增加 `AV_PIX_FMT_RGBAF32 → PixelFormat::F32``AV_PIX_FMT_RGBF32 → PixelFormat::F32`
- `FFmpegDecoder::GetNativeChannelCount()` 增加对应分支
- `IsPixelFormatGLSLCompatible()` 增加 `AV_PIX_FMT_RGBAF32`(可选,因为强制转换后解码器输出就是 RGBAF32)
3. **内部工作格式锁定 F32**
-`NodeTraverser` 初始化或 `RenderProcessor` 创建时,`SetCacheVideoParams()` 强制 `format = PixelFormat::F32`
- 移除用户层对工作格式的可选配置(或保留配置但忽略/默认 F32)
- `traverser.cpp``FootageJob``GenerateJob``ColorTransformJob` 的格式设置已经使用 `GetCacheVideoParams().format()`,因此只需确保基类参数是 F32 即可
4. **导出层适配**
- `FFmpegEncoder` 的输入当前通过 `avfilter` 图做格式转换,源为 F32 时:
- `FFmpegUtils::GetFFmpegPixelFormat(F32, 4)` 已返回 `AV_PIX_FMT_RGBAF32`
- 验证 filter graph 的 `buffer` source 和 `format` filter 能否正确处理 `RGBAF32`
- `RenderProcessor::GenerateFrame()` 下载 GPU texture 到 `FramePtr` 时,`DownloadFromTexture()` 已支持 `GL_FLOAT`,直接得到 F32 CPU buffer
## 4. 关键文件与修改清单
| 文件 | 修改内容 |
|------|----------|
| `app/codec/ffmpeg/ffmpegdecoder.cpp` | ① `GetNativePixelFormat()` 增加 RGBAF32/RGBF32 → F32 映射<br>② `GetNativeChannelCount()` 增加对应分支<br>③ `IsPixelFormatGLSLCompatible()` 增加 RGBAF32<br>④ `ProcessFrameIntoTexture()` 增加 RGBAF32 直接上传分支<br>⑤ `PreProcessFrame()` 确保 divider=1 且格式为 RGBAF32 时跳过 CPU 转换 |
| `app/codec/oiio/oiiodecoder.cpp` | `RetrieveVideoInternal()` 上传前若 `frame.format() != F32` 则调用 `convert(F32)` |
| `app/node/traverser.cpp``app/render/renderprocessor.cpp` | 初始化时强制 `SetCacheVideoParams().format = F32` |
| `app/codec/ffmpeg/ffmpegencoder.cpp` | 验证 filter graph 对 RGBAF32 source 的处理,必要时调整 |
| `app/render/opengl/openglrenderer.cpp` | 确认 `GL_RGBA32F / GL_FLOAT` 路径完整,补充必要错误检查 |
| `app/codec/ffmpeg/ffmpegutils.cpp` | 验证 `GetCompatiblePixelFormat(maximum=F32)` 的行为 |
## 5. 风险评估
| 风险 | 说明 | 缓解措施 |
|------|------|----------|
| 内存带宽 ×4 | F32 是 U8 的 4 倍、U16/F16 的 2 倍,显存和内存占用显著增加 | 这是预期代价;`OfflinePixelFormat` 机制可继续用于代理预览,降低分辨率同时用 F16 减少带宽 |
| FFmpeg swscale 对 RGBAF32 支持 | `sws_scale_frame` 是否能正确处理 `AV_PIX_FMT_RGBAF32` 作为目标格式需验证 | 先写单元测试验证;若不支持,可用 OIIO `Frame::convert()` 作为 fallback,或在 GPU 上通过 shader 做格式转换 |
| OFX 插件兼容性 | 大部分 OFX 插件支持 `kOfxBitDepthFloat`,但仍有少数可能只支持 U8/U16 | `PluginRenderer` 已有格式转换路径,F32 的支持比 F16 更成熟 |
| 性能回归 | YUV→RGB 原来在 GPU 走 shader,若强制先转 RGBAF32 再上传,可能需要调整流程 | YUV 素材仍保留 GPU shader 转换路径,只是 shader 输出目标 texture 格式改为 F32OpenGL 已支持 `GL_RGBA32F` 作为 render target |
| 缓存文件体积翻倍 | 帧缓存从 U8/U16 改为 F32 后,磁盘缓存体积增大 | 可接受;必要时调整缓存策略或压缩 |
## 6. 建议的实施顺序
1. **第一阶段:** 修复 `FFmpegDecoder` F32 映射 + 增加 RGBAF32 直接上传分支,编写解码器单元测试
2. **第二阶段:**`OIIODecoder` 添加强制 F32 转换
3. **第三阶段:** 锁定内部工作格式为 F32,验证节点图全链路
4. **第四阶段:** 验证导出编码路径,确认 filter graph 对 RGBAF32 的处理
5. **第五阶段:** 性能测试与回归测试
## 7. 决策点
- 是否保留 `OfflinePixelFormat = F16` 的代理降级机制?还是连 proxy 也强制 F32?
- 若保留代理降级,是否需要在解码器层根据 online/offline 模式选择输出格式?