fix: bug-fix sweep across node, audio, render, plugin subsystems

Node core:
- MathNode/TrigonometryNode combo strings realigned with Operation enums
- mathbase scalar/vector operand pick no longer uses bitwise type checks
- NodeSetPositionAndDependenciesRecursively moves dependencies again
- RemoveAllKeyframes undo actually restores keyframes
- NodeGroup GetInputName null-deref guard, passthrough ids use input id
- NodeValueTable::Has is an exact type match; tag fallback only for
  empty tags; kStrCombo/kPushButton get data type names
- delete_all_keyframes no longer loops forever on unparented keyframes;
  keyframe-load failures propagate; rational interpolation falls back to
  double; OpacityEffect no longer leaks its internal MathNode

Audio/footage:
- AudioVisualWaveform: GetSummaryFromTime underflow OOB read, TrimIn
  prepend length bookkeeping, OverwriteSums source channel indexing
- PanNode inserts the pan value into the sample job (keyframed pan
  works); OutputParamsChanged is emitted on device change; PortAudio
  device indices are validated before Pa_GetDeviceInfo
- Footage: AdjustTimeByLoopMode no longer hangs/UBs on degenerate
  lengths, GetStreamIndex bounds-checked, CheckFootage clears stale
  state on missing files, failed probes are not cached,
  FootageDescription::Load requires its own root element

Render/track:
- ViewerOutput pushes the tagged samples value; TrackList disconnects
  the track-height lambda; GetTrackFromReference validity check
- RenderManager dummy backend: null-initialized threads, guarded
  decoder-cache/timer paths; Renderer::Destroy releases color cache
  shaders/textures; unknown dynamic backends no longer alias to oakgl
- SharedMemoryRegion POSIX attach validates segment size; ReadMessage
  skips blank lines instead of failing; GC counter clamped;
  IsRenderingCustomRange implemented; TimeOffsetNode gets a true
  inverse OutputTimeAdjustment; zero-speed clips return the held frame

Plugin/nodes:
- OliveClip: stored default region of definition is honored, on-demand
  images are cached; OliveHost sets host identity properties and logs
  instead of showing modal dialogs offscreen; Plugin.h dead decls gone
- DespillNode guards graph-less use with Rec.709 fallback; description
  typos fixed (despill, swirl); mosaic applies when only one axis
  matches; Windows-only Project filename separator test fixed
This commit is contained in:
2026-07-17 08:26:48 +08:00
parent d9a4e27045
commit 2aa921b215
61 changed files with 731 additions and 254 deletions
+12 -1
View File
@@ -197,6 +197,8 @@ void AudioManager::SetOutputDevice(PaDeviceIndex device)
{
if (device == paNoDevice) {
qInfo() << "No output device found";
} else if (device < 0 || device >= Pa_GetDeviceCount()) {
qWarning() << "Invalid output audio device index:" << device;
} else {
qInfo() << "Setting output audio device to"
<< Pa_GetDeviceInfo(device)->name;
@@ -205,12 +207,16 @@ void AudioManager::SetOutputDevice(PaDeviceIndex device)
output_device_ = device;
CloseOutputStream();
emit OutputParamsChanged();
}
void AudioManager::SetInputDevice(PaDeviceIndex device)
{
if (device == paNoDevice) {
qInfo() << "No input device found";
} else if (device < 0 || device >= Pa_GetDeviceCount()) {
qWarning() << "Invalid input audio device index:" << device;
} else {
qInfo() << "Setting input audio device to"
<< Pa_GetDeviceInfo(device)->name;
@@ -407,7 +413,12 @@ PaStreamParameters AudioManager::GetPortAudioParams(const AudioParams &params,
p.device = device;
p.hostApiSpecificStreamInfo = nullptr;
p.sampleFormat = GetPortAudioSampleFormat(params.format());
p.suggestedLatency = Pa_GetDeviceInfo(device)->defaultLowOutputLatency;
if (device >= 0 && device < Pa_GetDeviceCount()) {
p.suggestedLatency = Pa_GetDeviceInfo(device)->defaultLowOutputLatency;
} else {
p.suggestedLatency = 0;
}
return p;
}
+17 -10
View File
@@ -168,8 +168,9 @@ void AudioVisualWaveform::OverwriteSums(const AudioVisualWaveform &sums,
size_t our_start_index =
time_to_samples(dest - virtual_start_, rate_dbl);
// Get our source sample
size_t their_start_index = time_to_samples(offset, rate_dbl);
// Get our source sample, indexing with the SOURCE's channel count
size_t their_start_index = std::floor(offset.toDouble() * rate_dbl) *
sums.channel_count();
if (their_start_index >= their_arr.size()) {
continue;
}
@@ -260,7 +261,11 @@ void AudioVisualWaveform::TrimIn(rational length)
}
}
length_ = qMax(rational(0), length_ - length);
if (!negative) {
length_ = qMax(rational(0), length_ - length);
}
// Prepending grows the data before the existing start, so the absolute
// end (which length_ tracks) is unchanged
}
AudioVisualWaveform AudioVisualWaveform::Mid(const rational &offset) const
@@ -321,14 +326,16 @@ AudioVisualWaveform::GetSummaryFromTime(const rational &start,
const Sample &mipmap_data = using_mipmap->second;
// Determine if the array actually has this sample
sample_length = qMin(sample_length, mipmap_data.size() - start_sample);
// Determine if the array actually has this sample. Compare in signed
// arithmetic so a start past the end of the data doesn't underflow.
qint64 available = qint64(mipmap_data.size()) - qint64(start_sample);
if (available > 0) {
sample_length = qMin(sample_length, size_t(available));
// Based on the above `min`, if sample length <= 0, that means start_sample >= the size of the
// array and nothing can be returned.
if (sample_length > 0) {
return ReSumSamples(&mipmap_data.data()[start_sample], sample_length,
channels_);
if (sample_length > 0) {
return ReSumSamples(&mipmap_data.data()[start_sample],
sample_length, channels_);
}
}
// Return null samples
+3 -2
View File
@@ -90,8 +90,9 @@ void PanNode::Value(const NodeValueRow &value, const NodeGlobals &globals,
table->Push(NodeValue(NodeValue::kSamples, samples, this));
} else {
// Requires job
table->Push(NodeValue::kSamples,
SampleJob(globals.time(), kSamplesInput, value),
SampleJob job(globals.time(), kSamplesInput, value);
job.Insert(kPanningInput, value);
table->Push(NodeValue::kSamples, QVariant::fromValue(job),
this);
}
} else {
+2 -2
View File
@@ -250,8 +250,8 @@ rational ClipBlock::MediaToSequenceTime(const rational &media_time) const
double speed_value = speed();
if (qIsNull(speed_value)) {
// I don't know what to return here yet...
sequence_time = rational::NaN;
// Speed zero holds the frame at the in point, so map to that frame
sequence_time = media_in();
} else if (!qFuzzyCompare(speed_value, 1.0)) {
// Divide time
sequence_time =
+1 -1
View File
@@ -71,7 +71,7 @@ QVector<Node::CategoryID> SwirlDistortNode::Category() const
QString SwirlDistortNode::Description() const
{
return tr("Distorts an image along a sine wave.");
return tr("Distorts an image by swirling it around a center point.");
}
void SwirlDistortNode::Retranslate()
@@ -32,6 +32,7 @@ const QString OpacityEffect::kValueInput = QStringLiteral("opacity_in");
OpacityEffect::OpacityEffect()
{
MathNode *math = new MathNode();
math->setParent(this);
math->SetOperation(MathNode::kOpMultiply);
+2 -2
View File
@@ -59,8 +59,8 @@ void MosaicFilterNode::Value(const NodeValueRow &value,
NodeValueTable *table) const
{
if (TexturePtr texture = value[kTextureInput].toTexture()) {
if (texture && value[kHorizInput].toInt() != texture->width() &&
value[kVertInput].toInt() != texture->height()) {
if (texture && (value[kHorizInput].toInt() != texture->width() ||
value[kVertInput].toInt() != texture->height())) {
ShaderJob job(value);
// Mipmapping makes this look weird, so we just use bilinear for finding the color of each block
+4 -1
View File
@@ -262,7 +262,7 @@ QString NodeGroup::AddInputPassthrough(const NodeInput &input,
id = input.input();
int i = 2;
while (HasInputWithID(id)) {
id = QStringLiteral("%1_%2").arg(input.name(), QString::number(i));
id = QStringLiteral("%1_%2").arg(input.input(), QString::number(i));
i++;
}
} else {
@@ -334,6 +334,9 @@ QString NodeGroup::GetInputName(const QString &id) const
// Call GetInputName of passed through node, which may be another group
NodeInput pass = GetInputFromID(id);
if (!pass.IsValid()) {
return QString();
}
return pass.node()->GetInputName(pass.input());
}
+14 -3
View File
@@ -23,6 +23,7 @@
#include "common/lerp.h"
#include "common/tohex.h"
#include "node.h"
namespace olive
{
@@ -289,11 +290,21 @@ void NodeInputImmediate::remove_keyframe(NodeKeyframe *key)
void NodeInputImmediate::delete_all_keyframes(QObject *parent)
{
for (NodeKeyframeTrack &track : keyframe_tracks_) {
while (!track.isEmpty()) {
// Iterate over a copy of the track, since the keyframes may be removed
// from it as we go
const NodeKeyframeTrack copy = track;
for (NodeKeyframe *key : copy) {
if (!dynamic_cast<Node *>(key->QObject::parent())) {
// Keyframe isn't parented to a node, so reparenting/deleting it
// won't remove it from the track automatically
remove_keyframe(key);
}
if (parent) {
track.first()->setParent(parent);
key->setParent(parent);
} else {
delete track.first();
delete key;
}
}
}
+8 -2
View File
@@ -61,7 +61,7 @@ QVector<Node::CategoryID> DespillNode::Category() const
QString DespillNode::Description() const
{
return tr("Selection of simple depsill operations");
return tr("Selection of simple despill operations");
}
void DespillNode::Retranslate()
@@ -95,7 +95,13 @@ void DespillNode::Value(const NodeValueRow &value, const NodeGlobals &globals,
// Set luma coefficients
double luma_coeffs[3] = { 0.0f, 0.0f, 0.0f };
project()->color_manager()->GetDefaultLumaCoefs(luma_coeffs);
if (project() && project()->color_manager()) {
project()->color_manager()->GetDefaultLumaCoefs(luma_coeffs);
} else {
luma_coeffs[0] = 0.2126;
luma_coeffs[1] = 0.7152;
luma_coeffs[2] = 0.0722;
}
job.Insert(
QStringLiteral("luma_coeffs"),
NodeValue(NodeValue::kVec3,
-1
View File
@@ -85,7 +85,6 @@ void MathNode::Retranslate()
GetOperationName(kOpSubtract),
GetOperationName(kOpMultiply),
GetOperationName(kOpDivide),
QString(),
GetOperationName(kOpPower) };
SetComboBoxStrings(kMethodIn, operations);
+6 -3
View File
@@ -323,11 +323,14 @@ void MathNodeBase::ValueInternal(
QVector4D vec = (NodeValue::type_is_vector(val_a.type()) ?
RetrieveVector(val_a) :
RetrieveVector(val_b));
float number =
RetrieveNumber((val_a.type() & NodeValue::kMatrix) ? val_b : val_a);
float number = RetrieveNumber(NodeValue::type_is_vector(val_a.type()) ?
val_b :
val_a);
// Only multiply and divide are valid operations
PushVector(output, val_a.type(),
PushVector(output,
NodeValue::type_is_vector(val_a.type()) ? val_a.type() :
val_b.type(),
PerformMultDiv<QVector4D, float>(operation, vec, number));
break;
}
@@ -64,11 +64,9 @@ void TrigonometryNode::Retranslate()
QStringList strings = { tr("Sine"),
tr("Cosine"),
tr("Tangent"),
QString(),
tr("Inverse Sine"),
tr("Inverse Cosine"),
tr("Inverse Tangent"),
QString(),
tr("Hyperbolic Sine"),
tr("Hyperbolic Cosine"),
tr("Hyperbolic Tangent") };
+17 -3
View File
@@ -497,8 +497,19 @@ QVariant Node::GetSplitValueAtTimeOnTrack(const QString &input,
double before_val, after_val, interpolated;
if (type == NodeValue::kRational) {
before_val = before->value().value<rational>().toDouble();
after_val = after->value().value<rational>().toDouble();
// Keys for rational inputs usually hold rationals, but may
// hold plain doubles, in which case we convert to rational
// first to preserve the value
before_val = (before->value().canConvert<rational>() ?
before->value().value<rational>() :
rational::fromDouble(
before->value().toDouble()))
.toDouble();
after_val = (after->value().canConvert<rational>() ?
after->value().value<rational>() :
rational::fromDouble(
after->value().toDouble()))
.toDouble();
} else {
before_val = before->value().toDouble();
after_val = after->value().toDouble();
@@ -1724,7 +1735,10 @@ bool Node::LoadImmediate(QXmlStreamReader *reader, const QString &input,
key->set_element(element);
key->set_track(track);
key->load(reader, data_type);
if (!key->load(reader, data_type)) {
delete key;
return false;
}
key->setParent(this);
} else {
reader->skipCurrentElement();
+6 -2
View File
@@ -109,7 +109,7 @@ void NodeSetPositionAndDependenciesRecursivelyCommand::move_recursively(
{
Node::Position pos = context_->GetNodePositionDataInContext(node);
pos += diff;
commands_.append(new NodeSetPositionCommand(node_, context_, pos));
commands_.append(new NodeSetPositionCommand(node, context_, pos));
for (auto it = node->input_connections().cbegin();
it != node->input_connections().cend(); it++) {
@@ -610,6 +610,10 @@ void NodeImmediateRemoveAllKeyframesCommand::prepare()
for (const NodeKeyframeTrack &track : immediate_->keyframe_tracks()) {
keys_.append(track);
}
if (!keys_.isEmpty()) {
node_ = keys_.first()->parent();
}
}
void NodeImmediateRemoveAllKeyframesCommand::redo()
@@ -622,7 +626,7 @@ void NodeImmediateRemoveAllKeyframesCommand::redo()
void NodeImmediateRemoveAllKeyframesCommand::undo()
{
for (auto it = keys_.crbegin(); it != keys_.crend(); it++) {
(*it)->setParent(&memory_manager_);
(*it)->setParent(node_);
}
}
+3
View File
@@ -823,6 +823,7 @@ class NodeImmediateRemoveAllKeyframesCommand : public UndoCommand {
public:
NodeImmediateRemoveAllKeyframesCommand(NodeInputImmediate *immediate)
: immediate_(immediate)
, node_(nullptr)
{
}
@@ -841,6 +842,8 @@ protected:
private:
NodeInputImmediate *immediate_;
Node *node_;
QObject memory_manager_;
QVector<NodeKeyframe *> keys_;
+6 -4
View File
@@ -85,10 +85,11 @@ void TrackList::TrackConnected(Node *node, int element)
connect(track, &Track::TrackLengthChanged, this,
&TrackList::UpdateTotalLength);
connect(track, &Track::TrackHeightChanged, this, [this]() {
Track *t = static_cast<Track *>(sender());
emit TrackHeightChanged(t, t->GetTrackHeightInPixels());
});
track_height_connections_.insert(
track, connect(track, &Track::TrackHeightChanged, this, [this]() {
Track *t = static_cast<Track *>(sender());
emit TrackHeightChanged(t, t->GetTrackHeightInPixels());
}));
track->set_type(type_);
track->set_sequence(parent());
@@ -134,6 +135,7 @@ void TrackList::TrackDisconnected(Node *node, int element)
disconnect(track, &Track::TrackLengthChanged, this,
&TrackList::UpdateTotalLength);
disconnect(track_height_connections_.take(track));
emit TrackListChanged();
+6
View File
@@ -22,6 +22,7 @@
#ifndef TRACKLIST_H
#define TRACKLIST_H
#include <QHash>
#include <QObject>
#include "node/output/track/track.h"
@@ -113,6 +114,11 @@ private:
QVector<Track *> track_cache_;
QVector<int> track_array_indexes_;
/**
* @brief Stored TrackHeightChanged connections so they can be disconnected again
*/
QHash<Track *, QMetaObject::Connection> track_height_connections_;
QString track_input_;
rational total_length_;
+1 -1
View File
@@ -460,7 +460,7 @@ void ViewerOutput::Value(const NodeValueRow &value, const NodeGlobals &globals,
if (HasInputWithID(kSamplesInput)) {
NodeValue repush = value[kSamplesInput];
repush.set_tag(Track::Reference(Track::kAudio, 0).ToString());
table->Push(value[kSamplesInput]);
table->Push(repush);
}
}
-3
View File
@@ -40,9 +40,6 @@ public:
QString SubCategory() const override;
QString Description() const override;
void AddPushButton();
void AddPage();
Node *copy() const override;
/**
* @brief The main processing function
+38 -14
View File
@@ -182,11 +182,20 @@ int Footage::GetStreamIndex(Track::Type type, int index) const
{
switch (type) {
case Track::kVideo:
return GetVideoParams(index).stream_index();
if (index >= 0 && index < GetVideoStreamCount()) {
return GetVideoParams(index).stream_index();
}
break;
case Track::kAudio:
return GetAudioParams(index).stream_index();
if (index >= 0 && index < GetAudioStreamCount()) {
return GetAudioParams(index).stream_index();
}
break;
case Track::kSubtitle:
return GetSubtitleParams(index).stream_index();
if (index >= 0 && index < GetSubtitleStreamCount()) {
return GetSubtitleParams(index).stream_index();
}
break;
case Track::kNone:
case Track::kCount:
break;
@@ -476,18 +485,29 @@ rational Footage::AdjustTimeByLoopMode(rational time, LoopMode loop_mode,
time = rational::NaN;
break;
case LoopMode::kLoopModeClamp:
// Clamp footage time to length
time = std::clamp(time, rational(0), length - timebase);
if (length < timebase) {
// No full frame fits in the range, so there is nothing to clamp to
time = rational::NaN;
} else {
// Clamp footage time to length
time = std::clamp(time, rational(0), length - timebase);
}
break;
case LoopMode::kLoopModeLoop:
// Loop footage time around job length
do {
if (time >= length) {
time -= length;
} else {
time += length;
}
} while (TimeIsOutOfBounds(time, length));
if (length <= 0) {
// Cannot loop around an empty range
time = rational::NaN;
} else {
// Loop footage time around job length
do {
if (time >= length) {
time -= length;
} else {
time += length;
}
} while (TimeIsOutOfBounds(time, length));
}
break;
}
}
@@ -797,7 +817,10 @@ void Footage::Reprobe()
}
if (!cancelled_ || !cancelled_->HeardCancel()) {
if (!footage_info.Save(meta_cache_file)) {
// Only cache successful probes; caching a failed probe
// would make every future load re-use the invalid metadata
if (footage_info.IsValid() &&
!footage_info.Save(meta_cache_file)) {
qWarning()
<< "Failed to save stream cache, footage will have to be re-probed";
}
@@ -898,6 +921,7 @@ void Footage::CheckFootage()
if (current_file_timestamp != timestamp()) {
// File has changed!
Clear();
Reprobe();
InvalidateAll(kFilenameInput);
}
@@ -41,8 +41,11 @@ bool FootageDescription::Load(const QString &filename)
if (file.open(QFile::ReadOnly)) {
QXmlStreamReader reader(&file);
bool found_streamcache = false;
while (XMLReadNextStartElement(&reader)) {
if (reader.name() == QStringLiteral("streamcache")) {
found_streamcache = true;
// Default to first version of metadata (which wasn't versioned at all)
unsigned version = 1;
@@ -126,7 +129,8 @@ bool FootageDescription::Load(const QString &filename)
qWarning() << "Failed to load footage description for" << filename
<< reader.errorString();
} else {
return true;
// Only accept files whose root element is the one Save() writes
return found_streamcache;
}
}
+3
View File
@@ -70,6 +70,9 @@ public:
Track *GetTrackFromReference(const Track::Reference &track_ref) const
{
if (track_ref.type() < 0 || track_ref.type() >= track_lists_.size()) {
return nullptr;
}
return track_lists_.at(track_ref.type())->GetTrackAt(track_ref.index());
}
+4 -2
View File
@@ -120,9 +120,11 @@ ProjectSerializer::Result ProjectSerializer::Load(Project *project,
if (attr.name() ==
QStringLiteral("version")) { // 230220+ projects
version = attr.value().toUInt();
} else if (reader->name() ==
} else if (attr.name() ==
QStringLiteral("url")) { // 230220+ projects
project->SetSavedURL(attr.value().toString());
if (project) {
project->SetSavedURL(attr.value().toString());
}
}
}
+13 -8
View File
@@ -66,14 +66,14 @@ TimeRange
TimeOffsetNode::OutputTimeAdjustment(const QString &input, int element,
const TimeRange &input_time) const
{
/*if (input == kInputInput) {
rational target_time = GetValueAtTime(kTimeInput, input_time.in()).value<rational>();
return TimeRange(target_time, target_time + input_time.length());
} else {
return super::OutputTimeAdjustment(input, element, input_time);
}*/
return super::OutputTimeAdjustment(input, element, input_time);
if (input == kInputInput) {
// The inverse of InputTimeAdjustment(): times at the input are mapped
// back to the output by subtracting the offset again
return TimeRange(GetRemappedOutputTime(input_time.in()),
GetRemappedOutputTime(input_time.out()));
} else {
return super::OutputTimeAdjustment(input, element, input_time);
}
}
void TimeOffsetNode::Value(const NodeValueRow &value,
@@ -88,4 +88,9 @@ rational TimeOffsetNode::GetRemappedTime(const rational &input) const
return input + GetValueAtTime(kTimeInput, input).value<rational>();
}
rational TimeOffsetNode::GetRemappedOutputTime(const rational &input) const
{
return input - GetValueAtTime(kTimeInput, input).value<rational>();
}
}
@@ -70,6 +70,7 @@ public:
private:
rational GetRemappedTime(const rational &input) const;
rational GetRemappedOutputTime(const rational &input) const;
};
}
+11 -6
View File
@@ -261,6 +261,8 @@ QString NodeValue::GetPrettyDataTypeName(Type type)
case kInt:
case kCombo:
return QCoreApplication::translate("NodeValue", "Integer");
case kStrCombo:
return QCoreApplication::translate("NodeValue", "String Combo");
case kFloat:
return QCoreApplication::translate("NodeValue", "Float");
case kRational:
@@ -297,6 +299,8 @@ QString NodeValue::GetPrettyDataTypeName(Type type)
return QCoreApplication::translate("NodeValue", "Subtitle Parameters");
case kBinary:
return QCoreApplication::translate("NodeValue", "Binary");
case kPushButton:
return QCoreApplication::translate("NodeValue", "Push Button");
case kDataTypeCount:
break;
@@ -314,6 +318,8 @@ QString NodeValue::GetDataTypeName(Type type)
return QStringLiteral("int");
case kCombo:
return QStringLiteral("combo");
case kStrCombo:
return QStringLiteral("strcombo");
case kFloat:
return QStringLiteral("float");
case kRational:
@@ -350,6 +356,8 @@ QString NodeValue::GetDataTypeName(Type type)
return QStringLiteral("sparam");
case kBinary:
return QStringLiteral("binary");
case kPushButton:
return QStringLiteral("pushbutton");
case kDataTypeCount:
break;
}
@@ -399,7 +407,7 @@ bool NodeValueTable::Has(NodeValue::Type type) const
for (int i = values_.size() - 1; i >= 0; i--) {
const NodeValue &v = values_.at(i);
if (v.type() & type) {
if (v.type() == type) {
return true;
}
}
@@ -463,12 +471,9 @@ int NodeValueTable::GetValueIndex(const QVector<NodeValue::Type> &types,
for (int i = values_.size() - 1; i >= 0; i--) {
const NodeValue &v = values_.at(i);
if (types.contains(v.type())) {
if (types.contains(v.type()) && (tag.isEmpty() || tag == v.tag())) {
index = i;
if (tag.isEmpty() || tag == v.tag()) {
break;
}
break;
}
}
+12
View File
@@ -467,7 +467,14 @@ olive::plugin::OliveClipInstance::getImage(OfxTime time,
return nullptr;
}
// Cache the on-demand image like the output path does, so repeated
// fetches at the same time reuse it and getConnected() reflects it.
// The extra reference keeps the cached image alive when the plugin
// releases its own.
pruneImagesCache();
Image *image = new Image(*this, preferred_params, bounds, rod, true);
images_.insert(time, image);
image->addReference();
return image;
}
}
@@ -546,6 +553,11 @@ olive::plugin::OliveClipInstance::getRegionOfDefinition(OfxTime time) const
double par = params_.pixel_aspect_ratio().toDouble();
regionOfDefinition.x2 = params_.width() * par;
regionOfDefinition.y2 = params_.height();
if (regionOfDefinition.x2 <= 0 || regionOfDefinition.y2 <= 0) {
// The params provide no usable region; fall back to the default set
// via setDefaultRegionOfDefinition().
return defaultRegionOfDefinitions_;
}
return regionOfDefinition;
}
void olive::plugin::OliveClipInstance::setRegionOfDefinition(
+44 -4
View File
@@ -33,6 +33,7 @@
#include "OlivePluginInstance.h"
#include "common/Current.h"
#include "ofxMessage.h"
#include "version.h"
#include <QMessageBox>
using namespace OFX::Host;
using namespace olive::plugin;
@@ -113,6 +114,26 @@ void olive::plugin::loadPlugins(QString path)
}
cache->scanPluginFiles();
}
OliveHost::OliveHost()
{
// Identify the host to plugins; HostSupport seeds these with "UNKNOWN".
_properties.setStringProperty(kOfxPropName, "Oak Video Editor");
_properties.setStringProperty(kOfxPropLabel, "Oak Video Editor");
_properties.setStringProperty(kOfxPropVersionLabel,
olive::kAppVersion.toStdString());
// Numeric version for plugins that query kOfxPropVersion directly.
const QStringList version_parts =
olive::kAppVersion.section(QLatin1Char('-'), 0, 0)
.split(QLatin1Char('.'));
_properties.setIntProperty(kOfxPropVersion,
version_parts.value(0).toInt(), 0);
_properties.setIntProperty(kOfxPropVersion,
version_parts.value(1).toInt(), 1);
_properties.setIntProperty(kOfxPropVersion,
version_parts.value(2).toInt(), 2);
}
OliveHost::~OliveHost()
{
}
@@ -184,7 +205,9 @@ OfxStatus olive::plugin::OliveHost::vmessage(const char *type, const char *id,
QString message(buffer);
auto *app = qobject_cast<QApplication *>(QCoreApplication::instance());
if (!app) {
// A modal dialog would hang a headless (offscreen) session, so log to
// stderr instead of showing one.
if (!app || QGuiApplication::platformName() == QLatin1String("offscreen")) {
qWarning().noquote() << "OFX message:" << type << message;
if (strcmp(type, kOfxMessageQuestion) == 0) {
return kOfxStatReplyNo;
@@ -223,15 +246,32 @@ OfxStatus olive::plugin::OliveHost::setPersistentMessage(const char *type,
vsnprintf(buffer, sizeof(buffer), format, args);
QString message(buffer);
// A modal dialog would hang a headless (offscreen) session, so log to
// stderr instead of showing one.
const bool headless =
QGuiApplication::platformName() == QLatin1String("offscreen");
if (strcmp(type, kOfxMessageError) == 0) {
persistent_messages_.append({ HostMessageType::Error, message });
QMessageBox::critical(nullptr, "", message);
if (headless) {
qWarning().noquote() << "OFX error:" << message;
} else {
QMessageBox::critical(nullptr, "", message);
}
} else if (strcmp(type, kOfxMessageWarning) == 0) {
persistent_messages_.append({ HostMessageType::Warning, message });
QMessageBox::warning(nullptr, "", message);
if (headless) {
qWarning().noquote() << "OFX warning:" << message;
} else {
QMessageBox::warning(nullptr, "", message);
}
} else if (strcmp(type, kOfxMessageMessage) == 0) {
persistent_messages_.append({ HostMessageType::Message, message });
QMessageBox::information(nullptr, "", message);
if (headless) {
qWarning().noquote() << "OFX message:" << message;
} else {
QMessageBox::information(nullptr, "", message);
}
} else {
return kOfxStatFailed;
}
+1 -1
View File
@@ -44,7 +44,7 @@ struct HostPersistentMessage {
void loadPlugins(QString path);
class OliveHost : public OFX::Host::ImageEffect::Host {
public:
OliveHost() = default;
OliveHost();
~OliveHost() override;
void destroyInstance(OFX::Host::ImageEffect::Instance *instance);
+10 -3
View File
@@ -37,9 +37,16 @@ DynamicRenderer::~DynamicRenderer()
// system libGL/libvulkan loader is never mistaken for an Oak render backend.
QString DynamicRenderer::LibraryFilename() const
{
const QString base = backend_ == QStringLiteral("vulkan") ?
QStringLiteral("oakvulkan") :
QStringLiteral("oakgl");
QString base;
if (backend_ == QStringLiteral("opengl")) {
base = QStringLiteral("oakgl");
} else if (backend_ == QStringLiteral("vulkan")) {
base = QStringLiteral("oakvulkan");
} else {
// Unknown backend: use the name verbatim so the load fails and the
// caller's OpenGL fallback engages
base = backend_;
}
#if defined(Q_OS_WIN)
const QString filename = base + QStringLiteral(".dll");
#elif defined(Q_OS_MAC)
+25 -26
View File
@@ -38,37 +38,36 @@ bool WriteMessage(QIODevice *device, const QJsonObject &obj)
bool ReadMessage(QByteArray *buffer, QJsonObject *out, bool *ok)
{
const int newline = buffer->indexOf('\n');
if (newline < 0) {
// No complete line buffered yet.
return false;
}
const QByteArray line = buffer->left(newline);
buffer->remove(0, newline + 1);
// Skip blank lines silently (e.g. a stray newline) without flagging an error.
if (line.trimmed().isEmpty()) {
if (ok) {
*ok = false;
while (true) {
const int newline = buffer->indexOf('\n');
if (newline < 0) {
// No complete line buffered yet.
return false;
}
return false;
}
QJsonParseError err;
const QJsonDocument doc = QJsonDocument::fromJson(line, &err);
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
if (ok) {
*ok = false;
const QByteArray line = buffer->left(newline);
buffer->remove(0, newline + 1);
// Skip blank lines silently (e.g. a stray newline) without flagging an error.
if (line.trimmed().isEmpty()) {
continue;
}
return false;
}
*out = doc.object();
if (ok) {
*ok = true;
QJsonParseError err;
const QJsonDocument doc = QJsonDocument::fromJson(line, &err);
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
if (ok) {
*ok = false;
}
return false;
}
*out = doc.object();
if (ok) {
*ok = true;
}
return true;
}
return true;
}
// ---- HandshakeMsg ---------------------------------------------------------------------------
+20
View File
@@ -166,6 +166,26 @@ bool SharedMemoryRegion::Open(const QString &key, size_t size, Mode mode)
shm_unlink(name_bytes.constData());
return false;
}
} else {
// mmap() succeeds even beyond the real segment size and only faults
// (SIGBUS) on access, so verify the segment is large enough up front.
struct stat st;
if (fstat(fd_, &st) != 0) {
error_ = QStringLiteral("fstat failed: %1")
.arg(QString::fromUtf8(strerror(errno)));
::close(fd_);
fd_ = -1;
return false;
}
if (st.st_size < off_t(size)) {
error_ = QStringLiteral(
"shared memory segment is %1 bytes, smaller than the requested %2")
.arg(qint64(st.st_size))
.arg(qint64(size));
::close(fd_);
fd_ = -1;
return false;
}
}
data_ = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0);
+10 -2
View File
@@ -504,8 +504,16 @@ void PreviewAutoCacher::CancelAudioTasks(bool and_wait_for_them_to_finish)
bool PreviewAutoCacher::IsRenderingCustomRange() const
{
/*const VideoCacheData &d = video_cache_data_.value(viewer_node_);
return d.iterator.IsCustomRange() && d.iterator.HasNext();*/
if (!use_custom_range_) {
return false;
}
for (const VideoJob &job : pending_video_jobs_) {
if (job.range == custom_autocache_range_ && job.iterator.HasNext()) {
return true;
}
}
return false;
}
+18 -5
View File
@@ -129,16 +129,24 @@ QVariant Renderer::GetDefaultShader()
void Renderer::Destroy()
{
destroyed_ = true;
if (lifetime_) {
lifetime_->alive = false;
}
if (!default_shader_.isNull()) {
DestroyNativeShader(default_shader_);
default_shader_.clear();
}
color_cache_.clear();
{
QMutexLocker locker(&color_cache_mutex_);
// Destroy the cached native shaders explicitly. The LUT textures are
// TexturePtrs whose destructors call DestroyTexture(), so the cache must
// be cleared while the renderer is still alive for those to be honored.
for (auto it = color_cache_.begin(); it != color_cache_.end(); it++) {
if (!it->compiled_shader.isNull()) {
DestroyNativeShader(it->compiled_shader);
}
}
color_cache_.clear();
}
if (!interlace_texture_.isNull()) {
DestroyNativeShader(interlace_texture_);
@@ -150,6 +158,11 @@ void Renderer::Destroy()
}
texture_cache_.clear();
destroyed_ = true;
if (lifetime_) {
lifetime_->alive = false;
}
DestroyInternal();
}
+1 -1
View File
@@ -26,7 +26,7 @@ namespace olive
void RenderJobTracker::insert(const TimeRange &range, JobTime job_time)
{
// First remove any ranges with this (code copied
// First remove any ranges that overlap this one (code copied from TimeRangeList::remove)
TimeRangeList::util_remove(&jobs_, range);
// Now append the job
+21 -3
View File
@@ -220,7 +220,12 @@ RenderTicketPtr RenderManager::RenderFrame(const RenderVideoParams &params)
}
if (worker_params.return_type == ReturnType::kNull) {
dry_run_thread_->AddTicket(ticket);
if (dry_run_thread_) {
dry_run_thread_->AddTicket(ticket);
} else {
// No render threads (e.g. dummy backend), finish without a result
ticket->Finish();
}
} else if (worker_pool_ &&
worker_pool_->SubmitFrame(ticket, worker_params)) {
return ticket;
@@ -247,13 +252,16 @@ RenderTicketPtr RenderManager::RenderAudio(const RenderAudioParams &params)
ticket->setProperty("aparam", QVariant::fromValue(params.audio_params));
ticket->setProperty("mode", params.mode);
if (params.generate_waveforms) {
if (params.generate_waveforms && !waveform_threads_.empty()) {
size_t thread_index = last_waveform_thread_ % waveform_threads_.size();
RenderThread *thread = waveform_threads_[thread_index];
thread->AddTicket(ticket);
last_waveform_thread_++;
} else {
} else if (audio_thread_) {
audio_thread_->AddTicket(ticket);
} else {
// No render threads (e.g. dummy backend), finish without a result
ticket->Finish();
}
return ticket;
@@ -278,6 +286,11 @@ void RenderManager::SetAggressiveGarbageCollection(bool enabled)
{
aggressive_gc_ += enabled ? 1 : -1;
// Clamp at zero so unbalanced disable calls can't drive the counter negative
if (aggressive_gc_ < 0) {
aggressive_gc_ = 0;
}
if (aggressive_gc_ > 0) {
decoder_clear_timer_->setInterval(kDecoderMaximumInactivityAggressive);
} else {
@@ -287,6 +300,11 @@ void RenderManager::SetAggressiveGarbageCollection(bool enabled)
void RenderManager::ClearOldDecoders()
{
if (!decoder_cache_) {
// No decoder cache exists on backends without a renderer (e.g. dummy)
return;
}
QMutexLocker locker(decoder_cache_->mutex());
qint64 min_age =
+3 -3
View File
@@ -255,11 +255,11 @@ private:
QTimer *decoder_clear_timer_;
RenderThread *dry_run_thread_;
RenderThread *audio_thread_;
RenderThread *dry_run_thread_ = nullptr;
RenderThread *audio_thread_ = nullptr;
std::vector<RenderThread *> waveform_threads_;
size_t last_waveform_thread_;
size_t last_waveform_thread_ = 0;
std::list<RenderThread *> render_threads_;