build: split the engine into liboakengine.so; worker drops the UI entirely

Physical split: app/{audio,cli,codec,common,config,node,pluginSupport,
render,task,timeline,undo,tool,shaders} plus coreengine, version and
ui/icons+colorcoding move to a new top-level engine/ tree, built as
liboakengine.so (shared). The render backends (oakgl/oakvulkan) move
with it and link the engine library instead of embedding a static
render-core subset (libolive-rendercore is gone).

- oak-render-worker now links liboakengine instead of the whole
  libolive-editor object set: 336MB -> 2.9MB, no Qt Widgets UI
- the editor links liboakengine for the engine and keeps only UI
  objects in libolive-editor
- install/packaging: GNUInstallDirs libdir on Linux, bundle copy on
  macOS, oakengine.dll staged for NSIS, AppImage validation entry
- fix backend lookup for the new layout: DynamicRenderer searched
  ../app but backends now live in engine/; a stale pre-split liboakgl
  in the build tree got dlopened instead, re-initialized and later
  destroyed the interposed engine statics (full-suite segfault at
  DialogSequenceParameterTab, found via gdb watchpoint)
This commit is contained in:
2026-07-20 03:23:28 +08:00
parent 026ff94b5e
commit 28c4426236
604 changed files with 243 additions and 172 deletions
+438
View File
@@ -0,0 +1,438 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_MEMORYPOOL_H
#define OAK_MEMORYPOOL_H
#include <memory>
#include <QApplication>
#include <QDateTime>
#include <QDebug>
#include <QMutex>
#include <QTimer>
#include <stdint.h>
#include "common/define.h"
namespace olive
{
/**
* @brief MemoryPool base class
*
* A custom memory system that allocates can allocate several objects in a large chunk (as opposed to several small
* allocations). Improves performance and memory consumption.
*
* As a class, this base is usable by setting the template to an object of your choosing. The pool will then allocate
* `(element_count * sizeof(T))` per arena. Arenas are allocated and destroyed on the fly - when an arena fills up,
* another is allocated.
*
* `Get()` will return an ElementPtr. The original desired data can be accessed through ElementPtr::data(). This data
* will belong to the caller until ElementPtr goes out of scope and the memory is freed back into the pool.
*/
class MemoryPool : public QObject {
Q_OBJECT
public:
/**
* @brief Constructor
* @param element_count
*
* Number of elements per arena
*/
MemoryPool(int element_count)
{
element_count_ = element_count;
clear_timer_ = new QTimer();
clear_timer_->setInterval(kMaxEmptyArenaLife);
clear_timer_->moveToThread(qApp->thread());
connect(clear_timer_, &QTimer::timeout, this,
&MemoryPool::ClearEmptyArenas, Qt::DirectConnection);
QMetaObject::invokeMethod(clear_timer_, "start", Qt::QueuedConnection);
}
/**
* @brief Destructor
*
* Deletes all arenas.
*/
virtual ~MemoryPool()
{
Clear();
clear_timer_->deleteLater();
}
DISABLE_COPY_MOVE(MemoryPool)
/**
* @brief Clears all arenas, freeing all of their memory
*
* Note that this function is not safe, any elements that are still out there will be invalid
* and accessing them will cause a crash. You'll need to make sure all elements are already
* relinquished before then.
*/
void Clear()
{
qDeleteAll(arenas_);
arenas_.clear();
}
/**
* @brief Returns whether any arenas are successfully allocated
*/
inline bool IsAllocated() const
{
return !arenas_.empty();
}
/**
* @brief Returns current number of allocated arenas
*/
inline int GetArenaCount() const
{
return arenas_.size();
}
class Arena;
/**
* @brief A handle for a chunk of memory in an arena
*
* Calling Get() on the pool or arena will return a shared pointer to an element which will contain a pointer to
* the desired object/data in data(). When Element is destroyed (i.e. when ElementPtr goes out of scope), the memory
* is released back into the pool so it can be used by another class.
*/
class Element {
public:
/**
* @brief Element Constructor
*
* There is no need to use this outside of the memory pool's internal functions.
*/
Element(Arena *parent, uint8_t *data)
{
parent_ = parent;
data_ = data;
accessed_ = QDateTime::currentMSecsSinceEpoch();
}
/**
* @brief Element Destructor
*
* Automatically releases this element's memory back to the arena it was retrieved from.
*/
~Element()
{
release();
}
DISABLE_COPY_MOVE(Element)
/**
* @brief Access data represented in the pool
*/
inline uint8_t *data() const
{
return data_;
}
inline const int64_t &timestamp() const
{
return timestamp_;
}
inline void set_timestamp(const int64_t &timestamp)
{
timestamp_ = timestamp;
}
/**
* @brief Register that this element has been accessed
*
* \see last_accessed()
*/
inline void access()
{
accessed_ = QDateTime::currentMSecsSinceEpoch();
}
/**
* @brief Returns the last time `access()` was called on this function
*
* Useful for determining the relative age of an element (i.e. if it hasn't been accessed for a certain amount of
* time, it can probably be freed back into the pool). This requires all usages to call `access()`.
*/
inline const int64_t &last_accessed() const
{
return accessed_;
}
void release()
{
if (data_) {
parent_->Release(this);
data_ = nullptr;
}
}
private:
Arena *parent_;
uint8_t *data_;
int64_t timestamp_;
int64_t accessed_;
};
using ElementPtr = std::shared_ptr<Element>;
/**
* @brief A memory pool arena - a subsection of memory
*
* The pool itself does not store memory, it stores "arenas". This is so that the pool can handle the situation of
* an arena becoming full with no more memory to lend. A pool can automatically allocate another arena and continue
* providing memory (and freeing arenas when they're no longer in use).
*/
class Arena {
public:
Arena(MemoryPool *parent)
{
parent_ = parent;
data_ = nullptr;
allocated_sz_ = 0;
empty_time_ = QDateTime::currentMSecsSinceEpoch();
}
~Arena()
{
std::list<Element *> copy = lent_elements_;
foreach (Element *e, copy) {
e->release();
}
delete[] data_;
}
DISABLE_COPY_MOVE(Arena)
/**
* @brief Returns an element if there is free memory to do so
*/
ElementPtr Get()
{
QMutexLocker locker(&lock_);
for (int i = 0; i < available_.size(); i++) {
if (available_.at(i)) {
// This buffer is available
available_.replace(i, false);
ElementPtr e = std::make_shared<Element>(
this,
reinterpret_cast<uint8_t *>(data_ + i * element_sz_));
lent_elements_.push_back(e.get());
return e;
}
}
return nullptr;
}
/**
* @brief Releases an element back into the pool for use elsewhere
*/
void Release(Element *e)
{
QMutexLocker locker(&lock_);
quintptr diff = reinterpret_cast<quintptr>(e->data()) -
reinterpret_cast<quintptr>(data_);
int index = diff / element_sz_;
available_.replace(index, true);
lent_elements_.remove(e);
if (lent_elements_.empty()) {
empty_time_ = QDateTime::currentMSecsSinceEpoch();
}
}
int GetUsageCount()
{
QMutexLocker locker(&lock_);
return lent_elements_.size();
}
bool Allocate(size_t ele_sz, size_t nb_elements)
{
if (IsAllocated()) {
return true;
}
element_sz_ = ele_sz;
allocated_sz_ = element_sz_ * nb_elements;
if ((data_ = new uint8_t[allocated_sz_])) {
available_.resize(nb_elements);
available_.fill(true);
return true;
} else {
available_.clear();
return false;
}
}
inline int GetElementCount() const
{
return available_.size();
}
inline bool IsAllocated() const
{
return data_;
}
inline qint64 GetTimeArenaWasMadeEmpty()
{
QMutexLocker locker(&lock_);
return empty_time_;
}
private:
MemoryPool *parent_;
uint8_t *data_;
size_t allocated_sz_;
QVector<bool> available_;
QMutex lock_;
size_t element_sz_;
std::list<Element *> lent_elements_;
qint64 empty_time_;
};
/**
* @brief Retrieves an element from an available arena
*/
ElementPtr Get()
{
QMutexLocker locker(&lock_);
// Attempt to get an element from an arena
foreach (Arena *a, arenas_) {
ElementPtr e = a->Get();
if (e) {
return e;
}
}
// All arenas were empty, we'll need to create a new one
if (arenas_.empty()) {
qDebug() << "No arenas, creating new...";
} else {
qDebug() << "All arenas are full, creating new...";
}
size_t ele_sz = GetElementSize();
if (!ele_sz) {
qCritical() << "Failed to create arena, element size was 0";
return nullptr;
}
if (element_count_ <= 0) {
qCritical() << "Failed to create arena, element count was invalid:"
<< element_count_;
return nullptr;
}
Arena *a = new Arena(this);
if (!a->Allocate(ele_sz, element_count_)) {
qCritical()
<< "Failed to create arena, allocation failed. Out of memory?";
delete a;
return nullptr;
}
arenas_.push_back(a);
return a->Get();
}
protected:
/**
* @brief The size of each element
*
* Override this to use a custom size (e.g. a char array where T = char but the element size is > 1)
*/
virtual size_t GetElementSize()
{
return sizeof(uint8_t);
}
private:
int element_count_;
std::list<Arena *> arenas_;
QMutex lock_;
QTimer *clear_timer_;
static const qint64 kMaxEmptyArenaLife = 5000;
private slots:
void ClearEmptyArenas()
{
QMutexLocker locker(&lock_);
const qint64 min_time =
QDateTime::currentMSecsSinceEpoch() - kMaxEmptyArenaLife;
for (auto it = arenas_.begin(); it != arenas_.end();) {
Arena *arena = (*it);
if (arena->GetUsageCount() == 0 &&
arena->GetTimeArenaWasMadeEmpty() <= min_time) {
qDebug() << "Removing an empty arena";
delete arena;
it = arenas_.erase(it);
} else {
it++;
}
}
}
};
}
#endif // OAK_MEMORYPOOL_H