Add render worker IPC loop
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
add_subdirectory(job)
|
||||
add_subdirectory(ipc)
|
||||
add_subdirectory(ocioconf)
|
||||
add_subdirectory(opengl)
|
||||
add_subdirectory(plugin)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Oak - Non-Linear Video Editor
|
||||
# Copyright (C) 2026 Oak Team
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
render/ipc/frameslotpool.cpp
|
||||
render/ipc/frameslotpool.h
|
||||
render/ipc/ipcmessage.cpp
|
||||
render/ipc/ipcmessage.h
|
||||
render/ipc/sharedmemoryregion.cpp
|
||||
render/ipc/sharedmemoryregion.h
|
||||
render/ipc/spscringbuffer.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -0,0 +1,173 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
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/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "frameslotpool.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Round `value` up to the next multiple of `align` (align must be a power of two).
|
||||
size_t AlignUp(size_t value, size_t align)
|
||||
{
|
||||
return (value + (align - 1)) & ~(align - 1);
|
||||
}
|
||||
|
||||
constexpr size_t kAlign = 64; // Cache-line alignment for each sub-region.
|
||||
|
||||
} // namespace
|
||||
|
||||
size_t FrameSlotPool::BytesNeeded(uint32_t slot_count, size_t slot_data_bytes)
|
||||
{
|
||||
const uint32_t ring_cap = RingCapacity(slot_count);
|
||||
size_t total = AlignUp(sizeof(Header), kAlign);
|
||||
total += AlignUp(SpscRingBuffer::BytesNeeded(ring_cap), kAlign); // free ring
|
||||
total += AlignUp(SpscRingBuffer::BytesNeeded(ring_cap), kAlign); // ready ring
|
||||
total += AlignUp(sizeof(FrameSlotMeta) * slot_count, kAlign); // metadata array
|
||||
total += AlignUp(slot_data_bytes, kAlign) * slot_count; // pixel data blocks
|
||||
return total;
|
||||
}
|
||||
|
||||
FrameSlotPool FrameSlotPool::Create(void *mem, uint32_t slot_count,
|
||||
size_t slot_data_bytes)
|
||||
{
|
||||
FrameSlotPool pool;
|
||||
pool.base_ = reinterpret_cast<uint8_t *>(mem);
|
||||
|
||||
const uint32_t ring_cap = RingCapacity(slot_count);
|
||||
|
||||
size_t offset = 0;
|
||||
const size_t header_off = offset;
|
||||
offset += AlignUp(sizeof(Header), kAlign);
|
||||
|
||||
const size_t free_off = offset;
|
||||
offset += AlignUp(SpscRingBuffer::BytesNeeded(ring_cap), kAlign);
|
||||
|
||||
const size_t ready_off = offset;
|
||||
offset += AlignUp(SpscRingBuffer::BytesNeeded(ring_cap), kAlign);
|
||||
|
||||
const size_t meta_off = offset;
|
||||
offset += AlignUp(sizeof(FrameSlotMeta) * slot_count, kAlign);
|
||||
|
||||
const size_t data_off = offset;
|
||||
|
||||
pool.header_ = reinterpret_cast<Header *>(pool.base_ + header_off);
|
||||
pool.header_->magic = kMagic;
|
||||
pool.header_->slot_count = slot_count;
|
||||
pool.header_->slot_data_bytes = slot_data_bytes;
|
||||
pool.header_->free_ring_offset = free_off;
|
||||
pool.header_->ready_ring_offset = ready_off;
|
||||
pool.header_->meta_offset = meta_off;
|
||||
pool.header_->data_offset = data_off;
|
||||
|
||||
pool.free_ring_ = SpscRingBuffer::Create(pool.base_ + free_off, ring_cap);
|
||||
pool.ready_ring_ = SpscRingBuffer::Create(pool.base_ + ready_off, ring_cap);
|
||||
pool.meta_ = reinterpret_cast<FrameSlotMeta *>(pool.base_ + meta_off);
|
||||
pool.data_ = pool.base_ + data_off;
|
||||
|
||||
memset(pool.meta_, 0, sizeof(FrameSlotMeta) * slot_count);
|
||||
|
||||
// Seed the free ring with every slot index so the filler can Acquire() immediately.
|
||||
for (uint32_t i = 0; i < slot_count; i++) {
|
||||
pool.free_ring_->Push(i);
|
||||
}
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
FrameSlotPool FrameSlotPool::Attach(void *mem)
|
||||
{
|
||||
FrameSlotPool pool;
|
||||
pool.base_ = reinterpret_cast<uint8_t *>(mem);
|
||||
pool.header_ = reinterpret_cast<Header *>(pool.base_);
|
||||
|
||||
if (pool.header_->magic != kMagic) {
|
||||
// Caller will see IsValid() == false via a null header reset.
|
||||
pool.header_ = nullptr;
|
||||
pool.base_ = nullptr;
|
||||
return pool;
|
||||
}
|
||||
|
||||
pool.free_ring_ = SpscRingBuffer::Attach(pool.base_ + pool.header_->free_ring_offset);
|
||||
pool.ready_ring_ = SpscRingBuffer::Attach(pool.base_ + pool.header_->ready_ring_offset);
|
||||
pool.meta_ = reinterpret_cast<FrameSlotMeta *>(pool.base_ + pool.header_->meta_offset);
|
||||
pool.data_ = pool.base_ + pool.header_->data_offset;
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
uint32_t FrameSlotPool::slot_count() const
|
||||
{
|
||||
return header_ ? header_->slot_count : 0;
|
||||
}
|
||||
|
||||
size_t FrameSlotPool::slot_data_bytes() const
|
||||
{
|
||||
return header_ ? size_t(header_->slot_data_bytes) : 0;
|
||||
}
|
||||
|
||||
bool FrameSlotPool::Acquire(uint32_t *index)
|
||||
{
|
||||
return free_ring_->Pop(index);
|
||||
}
|
||||
|
||||
void *FrameSlotPool::SlotData(uint32_t index)
|
||||
{
|
||||
return data_ + size_t(index) * AlignUp(slot_data_bytes(), kAlign);
|
||||
}
|
||||
|
||||
const void *FrameSlotPool::SlotData(uint32_t index) const
|
||||
{
|
||||
return data_ + size_t(index) * AlignUp(slot_data_bytes(), kAlign);
|
||||
}
|
||||
|
||||
FrameSlotMeta *FrameSlotPool::Meta(uint32_t index)
|
||||
{
|
||||
return &meta_[index];
|
||||
}
|
||||
|
||||
const FrameSlotMeta *FrameSlotPool::Meta(uint32_t index) const
|
||||
{
|
||||
return &meta_[index];
|
||||
}
|
||||
|
||||
bool FrameSlotPool::Publish(uint32_t index)
|
||||
{
|
||||
return ready_ring_->Push(index);
|
||||
}
|
||||
|
||||
bool FrameSlotPool::Consume(uint32_t *index)
|
||||
{
|
||||
return ready_ring_->Pop(index);
|
||||
}
|
||||
|
||||
bool FrameSlotPool::Release(uint32_t index)
|
||||
{
|
||||
return free_ring_->Push(index);
|
||||
}
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace olive
|
||||
@@ -0,0 +1,180 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
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 IPC_FRAMESLOTPOOL_H
|
||||
#define IPC_FRAMESLOTPOOL_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "spscringbuffer.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Per-slot metadata describing the frame currently occupying a slot.
|
||||
*
|
||||
* Trivially-copyable POD that lives in shared memory alongside the pixel data. Carries everything
|
||||
* the consumer needs to reconstruct an olive::Frame without any out-of-band information. We store
|
||||
* the rational timestamp as an explicit numerator/denominator pair to stay POD (olive::rational is
|
||||
* not guaranteed shared-memory-safe).
|
||||
*/
|
||||
struct FrameSlotMeta {
|
||||
int64_t id; ///< Caller-defined tag (e.g. ticket id, or footage stream hash).
|
||||
int64_t time_num; ///< Frame timestamp numerator.
|
||||
int64_t time_den; ///< Frame timestamp denominator.
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
int32_t format; ///< olive::PixelFormat::Format value.
|
||||
int32_t channel_count;
|
||||
int32_t linesize; ///< Bytes per scanline (stride).
|
||||
int32_t data_size; ///< Valid bytes written into the slot's data block.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A fixed-size pool of equal-sized frame slots in shared memory, with lock-free hand-off.
|
||||
*
|
||||
* One pool models a single direction of frame flow (e.g. worker -> main for rendered output, or
|
||||
* main -> worker for decoded input). Ownership of a slot is transferred via two SPSC ring buffers
|
||||
* of slot indices, so no mutex is ever taken:
|
||||
*
|
||||
* - free_ring: indices of slots available to the FILLER. The drainer returns slots here.
|
||||
* - ready_ring: indices of slots holding a published frame, produced by the FILLER for the
|
||||
* DRAINER to consume.
|
||||
*
|
||||
* Lifecycle (filler = producer of frames, drainer = consumer of frames):
|
||||
* filler: Acquire() -> pop a free index -> write meta + pixels -> Publish() -> push to ready
|
||||
* drainer: Consume() -> pop a ready index -> read meta + pixels -> Release() -> push to free
|
||||
*
|
||||
* Because each ring has exactly one producer and one consumer (the filler owns free.Pop +
|
||||
* ready.Push, the drainer owns ready.Pop + free.Push), the SPSC invariant holds and the whole
|
||||
* exchange is lock-free.
|
||||
*
|
||||
* All slots are sized to `slot_data_bytes`, computed for the maximum supported frame (e.g. 8K RGBA
|
||||
* half-float). Frames smaller than that simply use a prefix of the slot.
|
||||
*
|
||||
* The pool does NOT own the memory; it is constructed over a SharedMemoryRegion mapping. Use
|
||||
* BytesNeeded() to size that region.
|
||||
*/
|
||||
class FrameSlotPool {
|
||||
public:
|
||||
/**
|
||||
* @brief Total bytes a region must provide to back a pool of `slot_count` x `slot_data_bytes`.
|
||||
*/
|
||||
static size_t BytesNeeded(uint32_t slot_count, size_t slot_data_bytes);
|
||||
|
||||
/**
|
||||
* @brief Lay out and initialize a brand-new pool over `mem` (owner side, once).
|
||||
*
|
||||
* Initializes both rings, seeds the free ring with every slot index, and zeroes metadata.
|
||||
* `mem` must provide at least BytesNeeded(slot_count, slot_data_bytes) bytes.
|
||||
*/
|
||||
static FrameSlotPool Create(void *mem, uint32_t slot_count, size_t slot_data_bytes);
|
||||
|
||||
/**
|
||||
* @brief Map an existing, already-initialized pool (peer side).
|
||||
*
|
||||
* Reads slot_count/slot_data_bytes from the in-memory header written by Create().
|
||||
*/
|
||||
static FrameSlotPool Attach(void *mem);
|
||||
|
||||
bool IsValid() const
|
||||
{
|
||||
return header_ != nullptr;
|
||||
}
|
||||
|
||||
uint32_t slot_count() const;
|
||||
size_t slot_data_bytes() const;
|
||||
|
||||
// ---- Filler side ----
|
||||
|
||||
/**
|
||||
* @brief Take ownership of a free slot. Returns false (and leaves *index untouched) if none free.
|
||||
*/
|
||||
bool Acquire(uint32_t *index);
|
||||
|
||||
/**
|
||||
* @brief Pointer to a slot's pixel data block (slot_data_bytes available).
|
||||
*/
|
||||
void *SlotData(uint32_t index);
|
||||
|
||||
/**
|
||||
* @brief Mutable metadata for a slot. Filler writes this before Publish().
|
||||
*/
|
||||
FrameSlotMeta *Meta(uint32_t index);
|
||||
|
||||
/**
|
||||
* @brief Publish a filled slot to the drainer. Must follow a successful Acquire() of `index`.
|
||||
*/
|
||||
bool Publish(uint32_t index);
|
||||
|
||||
// ---- Drainer side ----
|
||||
|
||||
/**
|
||||
* @brief Take the next published slot. Returns false if nothing is ready.
|
||||
*/
|
||||
bool Consume(uint32_t *index);
|
||||
|
||||
/**
|
||||
* @brief Return a consumed slot to the free pool for reuse. Must follow Consume() of `index`.
|
||||
*/
|
||||
bool Release(uint32_t index);
|
||||
|
||||
const FrameSlotMeta *Meta(uint32_t index) const;
|
||||
const void *SlotData(uint32_t index) const;
|
||||
|
||||
private:
|
||||
FrameSlotPool() = default;
|
||||
|
||||
struct Header {
|
||||
uint32_t magic;
|
||||
uint32_t slot_count;
|
||||
uint64_t slot_data_bytes;
|
||||
// Byte offsets from the start of the segment to each sub-region.
|
||||
uint64_t free_ring_offset;
|
||||
uint64_t ready_ring_offset;
|
||||
uint64_t meta_offset;
|
||||
uint64_t data_offset;
|
||||
};
|
||||
|
||||
static constexpr uint32_t kMagic = 0x4F4B5350; // 'OKSP'
|
||||
|
||||
// Ring capacity must exceed slot_count by one because a ring can hold at most capacity-1 entries
|
||||
// and we need to be able to enqueue every slot at once.
|
||||
static uint32_t RingCapacity(uint32_t slot_count)
|
||||
{
|
||||
return slot_count + 1;
|
||||
}
|
||||
|
||||
uint8_t *base_ = nullptr;
|
||||
Header *header_ = nullptr;
|
||||
SpscRingBuffer *free_ring_ = nullptr;
|
||||
SpscRingBuffer *ready_ring_ = nullptr;
|
||||
FrameSlotMeta *meta_ = nullptr;
|
||||
uint8_t *data_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace olive
|
||||
|
||||
#endif // IPC_FRAMESLOTPOOL_H
|
||||
@@ -0,0 +1,195 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
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/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "ipcmessage.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QIODevice>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
bool WriteMessage(QIODevice *device, const QJsonObject &obj)
|
||||
{
|
||||
QByteArray line = QJsonDocument(obj).toJson(QJsonDocument::Compact);
|
||||
line.append('\n');
|
||||
return device->write(line) == line.size();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ---- HandshakeMsg ---------------------------------------------------------------------------
|
||||
|
||||
QJsonObject HandshakeMsg::ToJson() const
|
||||
{
|
||||
QJsonObject o;
|
||||
o["type"] = msgtype::kHandshake;
|
||||
o["protocol_version"] = protocol_version;
|
||||
o["shm_key"] = shm_key;
|
||||
o["input_slots"] = input_slots;
|
||||
o["output_slots"] = output_slots;
|
||||
o["slot_data_bytes"] = double(slot_data_bytes);
|
||||
return o;
|
||||
}
|
||||
|
||||
bool HandshakeMsg::FromJson(const QJsonObject &o, HandshakeMsg *out)
|
||||
{
|
||||
if (o["type"].toString() != QLatin1String(msgtype::kHandshake)) {
|
||||
return false;
|
||||
}
|
||||
out->protocol_version = o["protocol_version"].toInt();
|
||||
out->shm_key = o["shm_key"].toString();
|
||||
out->input_slots = o["input_slots"].toInt();
|
||||
out->output_slots = o["output_slots"].toInt();
|
||||
out->slot_data_bytes = qint64(o["slot_data_bytes"].toDouble());
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- RenderFrameMsg -------------------------------------------------------------------------
|
||||
|
||||
QJsonObject RenderFrameMsg::ToJson() const
|
||||
{
|
||||
QJsonObject o;
|
||||
o["type"] = msgtype::kRenderFrame;
|
||||
o["ticket"] = double(ticket_id);
|
||||
o["node"] = node_uuid;
|
||||
o["time_num"] = double(time_num);
|
||||
o["time_den"] = double(time_den);
|
||||
o["width"] = width;
|
||||
o["height"] = height;
|
||||
o["format"] = format;
|
||||
o["channels"] = channel_count;
|
||||
o["mode"] = mode;
|
||||
return o;
|
||||
}
|
||||
|
||||
bool RenderFrameMsg::FromJson(const QJsonObject &o, RenderFrameMsg *out)
|
||||
{
|
||||
if (o["type"].toString() != QLatin1String(msgtype::kRenderFrame)) {
|
||||
return false;
|
||||
}
|
||||
out->ticket_id = qint64(o["ticket"].toDouble());
|
||||
out->node_uuid = o["node"].toString();
|
||||
out->time_num = qint64(o["time_num"].toDouble());
|
||||
out->time_den = qint64(o["time_den"].toDouble(1));
|
||||
out->width = o["width"].toInt();
|
||||
out->height = o["height"].toInt();
|
||||
out->format = o["format"].toInt(-1);
|
||||
out->channel_count = o["channels"].toInt();
|
||||
out->mode = o["mode"].toInt();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- FrameReadyMsg --------------------------------------------------------------------------
|
||||
|
||||
QJsonObject FrameReadyMsg::ToJson() const
|
||||
{
|
||||
QJsonObject o;
|
||||
o["type"] = msgtype::kFrameReady;
|
||||
o["ticket"] = double(ticket_id);
|
||||
o["slot"] = output_slot;
|
||||
return o;
|
||||
}
|
||||
|
||||
bool FrameReadyMsg::FromJson(const QJsonObject &o, FrameReadyMsg *out)
|
||||
{
|
||||
if (o["type"].toString() != QLatin1String(msgtype::kFrameReady)) {
|
||||
return false;
|
||||
}
|
||||
out->ticket_id = qint64(o["ticket"].toDouble());
|
||||
out->output_slot = o["slot"].toInt();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- CancelMsg ------------------------------------------------------------------------------
|
||||
|
||||
QJsonObject CancelMsg::ToJson() const
|
||||
{
|
||||
QJsonObject o;
|
||||
o["type"] = msgtype::kCancel;
|
||||
o["ticket"] = double(ticket_id);
|
||||
return o;
|
||||
}
|
||||
|
||||
bool CancelMsg::FromJson(const QJsonObject &o, CancelMsg *out)
|
||||
{
|
||||
if (o["type"].toString() != QLatin1String(msgtype::kCancel)) {
|
||||
return false;
|
||||
}
|
||||
out->ticket_id = qint64(o["ticket"].toDouble());
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- LoadGraphMsg ---------------------------------------------------------------------------
|
||||
|
||||
QJsonObject LoadGraphMsg::ToJson() const
|
||||
{
|
||||
QJsonObject o;
|
||||
o["type"] = msgtype::kLoadGraph;
|
||||
o["path"] = path;
|
||||
return o;
|
||||
}
|
||||
|
||||
bool LoadGraphMsg::FromJson(const QJsonObject &o, LoadGraphMsg *out)
|
||||
{
|
||||
if (o["type"].toString() != QLatin1String(msgtype::kLoadGraph)) {
|
||||
return false;
|
||||
}
|
||||
out->path = o["path"].toString();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace olive
|
||||
@@ -0,0 +1,143 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
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 IPC_IPCMESSAGE_H
|
||||
#define IPC_IPCMESSAGE_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <QByteArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
class QIODevice;
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Control-plane protocol exchanged over stdio between main and render worker.
|
||||
*
|
||||
* The wire format is NDJSON: one compact QJsonObject per line, terminated by '\n'. This is
|
||||
* deliberately human-readable so the channel can be inspected live with `tee`/`cat` and test
|
||||
* messages can be injected by hand. The stdio channel carries only low-frequency control traffic;
|
||||
* bulk pixel data travels through the shared-memory FrameSlotPool, and the (potentially large)
|
||||
* serialized node graph travels via a temporary file referenced by path.
|
||||
*
|
||||
* Every message object has a "type" string field. Directionality (M = main, W = worker):
|
||||
* "handshake" M<->W Negotiate protocol version and announce shared-memory key/geometry.
|
||||
* "load_graph" M ->W Path to a temporary file holding the serialized node graph.
|
||||
* "render_frame" M ->W Request a frame: node uuid, time, video params.
|
||||
* "frame_ready" W ->M A rendered frame is published; carries the output-slot index + ticket.
|
||||
* "cancel" M ->W Abandon an in-flight ticket by id.
|
||||
* "graph_update" M ->W (Reserved, Phase 6) Incremental graph mutation, mirrors ProjectCopier.
|
||||
* "shutdown" M ->W Finish current work and exit cleanly.
|
||||
* "error" W ->M Worker-side failure report (human-readable "message" field).
|
||||
*/
|
||||
namespace msgtype
|
||||
{
|
||||
constexpr const char *kHandshake = "handshake";
|
||||
constexpr const char *kLoadGraph = "load_graph";
|
||||
constexpr const char *kRenderFrame = "render_frame";
|
||||
constexpr const char *kFrameReady = "frame_ready";
|
||||
constexpr const char *kCancel = "cancel";
|
||||
constexpr const char *kGraphUpdate = "graph_update";
|
||||
constexpr const char *kShutdown = "shutdown";
|
||||
constexpr const char *kError = "error";
|
||||
} // namespace msgtype
|
||||
|
||||
/**
|
||||
* @brief Write one NDJSON message line to `device`.
|
||||
*
|
||||
* Serializes `obj` to compact JSON, appends '\n', and writes the whole line in one call. Returns
|
||||
* true only if the full line was written.
|
||||
*/
|
||||
bool WriteMessage(QIODevice *device, const QJsonObject &obj);
|
||||
|
||||
/**
|
||||
* @brief Pull one complete NDJSON line out of `buffer` and parse it.
|
||||
*
|
||||
* If `buffer` contains at least one '\n', the leading line is removed, parsed as JSON, and returned
|
||||
* via `out` (true). If no complete line is buffered yet, leaves `buffer` untouched and returns
|
||||
* false. Malformed lines are skipped (removed) and reported via `*ok = false` so the reader can log
|
||||
* and continue rather than wedge. Supports the typical "append bytes as they arrive, then drain
|
||||
* complete lines" reader loop on a pipe.
|
||||
*/
|
||||
bool ReadMessage(QByteArray *buffer, QJsonObject *out, bool *ok = nullptr);
|
||||
|
||||
// ---- Typed message builders / parsers -------------------------------------------------------
|
||||
//
|
||||
// Thin helpers that construct or read the QJsonObject for each message type, keeping field names in
|
||||
// one place so main and worker agree. Fields use plain JSON numbers/strings; 64-bit ids are stored
|
||||
// as JSON numbers (doubles exactly represent integers up to 2^53, ample for our counters).
|
||||
|
||||
struct HandshakeMsg {
|
||||
int protocol_version = 0;
|
||||
QString shm_key; ///< Shared-memory segment key for this worker.
|
||||
int input_slots = 0; ///< Number of main->worker input frame slots.
|
||||
int output_slots = 0; ///< Number of worker->main output frame slots.
|
||||
qint64 slot_data_bytes = 0; ///< Per-slot pixel block size (max frame size).
|
||||
|
||||
QJsonObject ToJson() const;
|
||||
static bool FromJson(const QJsonObject &o, HandshakeMsg *out);
|
||||
};
|
||||
|
||||
struct RenderFrameMsg {
|
||||
qint64 ticket_id = 0; ///< Correlates this request with the eventual frame_ready.
|
||||
QString node_uuid; ///< Output/viewer node to render, by stable uuid in the loaded graph.
|
||||
qint64 time_num = 0;
|
||||
qint64 time_den = 1;
|
||||
int width = 0; ///< Forced output size (0 = use graph default).
|
||||
int height = 0;
|
||||
int format = -1; ///< Forced PixelFormat::Format (-1 = default/INVALID).
|
||||
int channel_count = 0; ///< 0 = default.
|
||||
int mode = 0; ///< RenderMode::Mode.
|
||||
|
||||
QJsonObject ToJson() const;
|
||||
static bool FromJson(const QJsonObject &o, RenderFrameMsg *out);
|
||||
};
|
||||
|
||||
struct FrameReadyMsg {
|
||||
qint64 ticket_id = 0;
|
||||
int output_slot = 0; ///< Index into the worker->main output FrameSlotPool.
|
||||
|
||||
QJsonObject ToJson() const;
|
||||
static bool FromJson(const QJsonObject &o, FrameReadyMsg *out);
|
||||
};
|
||||
|
||||
struct CancelMsg {
|
||||
qint64 ticket_id = 0;
|
||||
|
||||
QJsonObject ToJson() const;
|
||||
static bool FromJson(const QJsonObject &o, CancelMsg *out);
|
||||
};
|
||||
|
||||
struct LoadGraphMsg {
|
||||
QString path; ///< Temporary file holding the serialized node graph.
|
||||
|
||||
QJsonObject ToJson() const;
|
||||
static bool FromJson(const QJsonObject &o, LoadGraphMsg *out);
|
||||
};
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace olive
|
||||
|
||||
#endif // IPC_IPCMESSAGE_H
|
||||
@@ -0,0 +1,205 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
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/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "sharedmemoryregion.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <cstring>
|
||||
#endif
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
SharedMemoryRegion::SharedMemoryRegion()
|
||||
: size_(0)
|
||||
, data_(nullptr)
|
||||
, mode_(kAttach)
|
||||
#if defined(Q_OS_WIN)
|
||||
, handle_(nullptr)
|
||||
#else
|
||||
, fd_(-1)
|
||||
#endif
|
||||
{
|
||||
}
|
||||
|
||||
SharedMemoryRegion::~SharedMemoryRegion()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
QString SharedMemoryRegion::MakeKey(qint64 owner_pid, int worker_index)
|
||||
{
|
||||
return QStringLiteral("olive-rw-%1-%2").arg(owner_pid).arg(worker_index);
|
||||
}
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
|
||||
bool SharedMemoryRegion::Open(const QString &key, size_t size, Mode mode)
|
||||
{
|
||||
Close();
|
||||
|
||||
key_ = key;
|
||||
size_ = size;
|
||||
mode_ = mode;
|
||||
|
||||
// Windows global mapping names live in the Local\ namespace by default for the session.
|
||||
const QString mapping_name = QStringLiteral("Local\\") + key;
|
||||
const std::wstring wname = mapping_name.toStdWString();
|
||||
|
||||
if (mode == kCreate) {
|
||||
const DWORD size_high = static_cast<DWORD>((quint64(size) >> 32) & 0xFFFFFFFF);
|
||||
const DWORD size_low = static_cast<DWORD>(quint64(size) & 0xFFFFFFFF);
|
||||
handle_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE,
|
||||
size_high, size_low, wname.c_str());
|
||||
if (!handle_) {
|
||||
error_ = QStringLiteral("CreateFileMapping failed: %1").arg(GetLastError());
|
||||
return false;
|
||||
}
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
||||
error_ = QStringLiteral("Shared memory key already exists: %1").arg(key);
|
||||
CloseHandle(handle_);
|
||||
handle_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
handle_ = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, wname.c_str());
|
||||
if (!handle_) {
|
||||
error_ = QStringLiteral("OpenFileMapping failed: %1").arg(GetLastError());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
data_ = MapViewOfFile(handle_, FILE_MAP_ALL_ACCESS, 0, 0, size);
|
||||
if (!data_) {
|
||||
error_ = QStringLiteral("MapViewOfFile failed: %1").arg(GetLastError());
|
||||
CloseHandle(handle_);
|
||||
handle_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mode == kCreate) {
|
||||
memset(data_, 0, size);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SharedMemoryRegion::Close()
|
||||
{
|
||||
if (data_) {
|
||||
UnmapViewOfFile(data_);
|
||||
data_ = nullptr;
|
||||
}
|
||||
if (handle_) {
|
||||
CloseHandle(handle_);
|
||||
handle_ = nullptr;
|
||||
}
|
||||
size_ = 0;
|
||||
}
|
||||
|
||||
#else // POSIX
|
||||
|
||||
bool SharedMemoryRegion::Open(const QString &key, size_t size, Mode mode)
|
||||
{
|
||||
Close();
|
||||
|
||||
key_ = key;
|
||||
size_ = size;
|
||||
mode_ = mode;
|
||||
|
||||
// POSIX shared memory names must start with a single slash and contain no others.
|
||||
shm_name_ = QStringLiteral("/") + QString(key).replace('/', '_');
|
||||
const QByteArray name_bytes = shm_name_.toUtf8();
|
||||
|
||||
int oflag = O_RDWR;
|
||||
if (mode == kCreate) {
|
||||
oflag |= O_CREAT | O_EXCL;
|
||||
// Clear any stale segment left by a crashed previous run with the same name.
|
||||
shm_unlink(name_bytes.constData());
|
||||
}
|
||||
|
||||
fd_ = shm_open(name_bytes.constData(), oflag, 0600);
|
||||
if (fd_ < 0) {
|
||||
error_ = QStringLiteral("shm_open(%1) failed: %2")
|
||||
.arg(shm_name_, QString::fromUtf8(strerror(errno)));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mode == kCreate) {
|
||||
if (ftruncate(fd_, off_t(size)) != 0) {
|
||||
error_ = QStringLiteral("ftruncate failed: %1")
|
||||
.arg(QString::fromUtf8(strerror(errno)));
|
||||
::close(fd_);
|
||||
fd_ = -1;
|
||||
shm_unlink(name_bytes.constData());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
data_ = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0);
|
||||
if (data_ == MAP_FAILED) {
|
||||
error_ = QStringLiteral("mmap failed: %1").arg(QString::fromUtf8(strerror(errno)));
|
||||
data_ = nullptr;
|
||||
::close(fd_);
|
||||
fd_ = -1;
|
||||
if (mode == kCreate) {
|
||||
shm_unlink(name_bytes.constData());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mode == kCreate) {
|
||||
memset(data_, 0, size);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SharedMemoryRegion::Close()
|
||||
{
|
||||
if (data_) {
|
||||
munmap(data_, size_);
|
||||
data_ = nullptr;
|
||||
}
|
||||
if (fd_ >= 0) {
|
||||
::close(fd_);
|
||||
fd_ = -1;
|
||||
}
|
||||
if (mode_ == kCreate && !shm_name_.isEmpty()) {
|
||||
// Only the owner unlinks, so the name is freed once both sides have unmapped.
|
||||
shm_unlink(shm_name_.toUtf8().constData());
|
||||
shm_name_.clear();
|
||||
}
|
||||
size_ = 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace olive
|
||||
@@ -0,0 +1,123 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
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 IPC_SHAREDMEMORYREGION_H
|
||||
#define IPC_SHAREDMEMORYREGION_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <QString>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief A named, fixed-size shared memory segment mapped into the process address space.
|
||||
*
|
||||
* One process Create()s the segment (owner); the peer process Attach()es to it by the same key.
|
||||
* The mapping is a raw contiguous byte range accessible via data() — the IPC ring buffers and frame
|
||||
* slot pools are laid out inside it. Nothing here is locked; synchronization is entirely the
|
||||
* caller's responsibility via the lock-free structures placed in the mapping.
|
||||
*
|
||||
* We deliberately use the raw OS primitives (POSIX shm_open + mmap, Windows CreateFileMapping +
|
||||
* MapViewOfFile) rather than QSharedMemory: QSharedMemory carries an implicit semaphore and a 1-byte
|
||||
* header convention, attaches/detaches with reference counting we don't want, and historically has
|
||||
* cross-platform lifetime quirks. For a render pipeline pushing large frames we want a plain mmap.
|
||||
*/
|
||||
class SharedMemoryRegion {
|
||||
public:
|
||||
enum Mode {
|
||||
/// Create (and own) the segment. Fails if it already exists; unlinks on destruction.
|
||||
kCreate,
|
||||
/// Attach to a segment created by the peer. Does not unlink on destruction.
|
||||
kAttach
|
||||
};
|
||||
|
||||
SharedMemoryRegion();
|
||||
~SharedMemoryRegion();
|
||||
|
||||
SharedMemoryRegion(const SharedMemoryRegion &) = delete;
|
||||
SharedMemoryRegion &operator=(const SharedMemoryRegion &) = delete;
|
||||
|
||||
/**
|
||||
* @brief Open the segment identified by `key` with the given `size` in bytes.
|
||||
*
|
||||
* `key` is a short identifier (no leading slash needed; the platform prefix is added internally).
|
||||
* Returns true on success. On failure, error() carries a human-readable reason.
|
||||
*/
|
||||
bool Open(const QString &key, size_t size, Mode mode);
|
||||
|
||||
/**
|
||||
* @brief Unmap and (if owner) unlink the segment. Called automatically by the destructor.
|
||||
*/
|
||||
void Close();
|
||||
|
||||
bool IsValid() const
|
||||
{
|
||||
return data_ != nullptr;
|
||||
}
|
||||
|
||||
void *data() const
|
||||
{
|
||||
return data_;
|
||||
}
|
||||
|
||||
size_t size() const
|
||||
{
|
||||
return size_;
|
||||
}
|
||||
|
||||
const QString &key() const
|
||||
{
|
||||
return key_;
|
||||
}
|
||||
|
||||
const QString &error() const
|
||||
{
|
||||
return error_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build a unique segment key for a worker, e.g. "olive-rw-<pid>-<index>".
|
||||
*
|
||||
* Centralized so the owner and the spawned worker agree on the same name.
|
||||
*/
|
||||
static QString MakeKey(qint64 owner_pid, int worker_index);
|
||||
|
||||
private:
|
||||
QString key_;
|
||||
size_t size_;
|
||||
void *data_;
|
||||
Mode mode_;
|
||||
QString error_;
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
void *handle_; // HANDLE from CreateFileMapping/OpenFileMapping
|
||||
#else
|
||||
int fd_; // file descriptor from shm_open
|
||||
QString shm_name_; // the platform-prefixed name actually passed to shm_open
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace olive
|
||||
|
||||
#endif // IPC_SHAREDMEMORYREGION_H
|
||||
@@ -0,0 +1,184 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
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 IPC_SPSCRINGBUFFER_H
|
||||
#define IPC_SPSCRINGBUFFER_H
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief A lock-free single-producer / single-consumer ring buffer of uint32 indices.
|
||||
*
|
||||
* This is the core synchronization primitive for cross-process communication. It is designed to
|
||||
* live in a shared memory segment: the control block (head/tail cursors) and the slot array are a
|
||||
* single trivially-copyable, self-contained POD. Both the producer process and the consumer
|
||||
* process map the same memory and operate on it concurrently.
|
||||
*
|
||||
* Correctness relies on the classic SPSC invariant:
|
||||
* - Exactly ONE thread/process calls Push() (the producer).
|
||||
* - Exactly ONE thread/process calls Pop() (the consumer).
|
||||
* Under that invariant, no mutex is required. The producer only ever writes `head_`, the consumer
|
||||
* only ever writes `tail_`, and the acquire/release ordering on those two atomics publishes the
|
||||
* slot writes safely across the process boundary.
|
||||
*
|
||||
* Capacity note: one slot is always left empty to disambiguate the full and empty states, so a
|
||||
* buffer constructed with kCapacity slots can hold at most (kCapacity - 1) live entries.
|
||||
*
|
||||
* The payload stored is a single uint32_t per entry, intended to be an index into a separately
|
||||
* managed slot pool (see FrameSlotPool). We never put pointers in shared memory.
|
||||
*/
|
||||
class SpscRingBuffer {
|
||||
public:
|
||||
/**
|
||||
* @brief In-place construct a ring buffer header at `mem` with `capacity` index slots.
|
||||
*
|
||||
* `mem` must point to at least BytesNeeded(capacity) bytes of zero-initializable memory. This is
|
||||
* intended to be placement-style initialization performed exactly once by whichever process owns
|
||||
* the segment's creation; the peer process uses Attach() instead.
|
||||
*/
|
||||
static SpscRingBuffer *Create(void *mem, uint32_t capacity)
|
||||
{
|
||||
auto *self = reinterpret_cast<SpscRingBuffer *>(mem);
|
||||
self->capacity_ = capacity;
|
||||
self->head_.store(0, std::memory_order_relaxed);
|
||||
self->tail_.store(0, std::memory_order_relaxed);
|
||||
for (uint32_t i = 0; i < capacity; i++) {
|
||||
self->slot_array()[i] = 0;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Re-interpret already-initialized memory as a ring buffer (peer process side).
|
||||
*
|
||||
* No writes are performed; the cursors and capacity are assumed already set by Create().
|
||||
*/
|
||||
static SpscRingBuffer *Attach(void *mem)
|
||||
{
|
||||
return reinterpret_cast<SpscRingBuffer *>(mem);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Total bytes required to hold the header plus `capacity` index slots.
|
||||
*/
|
||||
static size_t BytesNeeded(uint32_t capacity)
|
||||
{
|
||||
return sizeof(SpscRingBuffer) + size_t(capacity) * sizeof(uint32_t);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Producer side: enqueue an index. Returns false if the buffer is full.
|
||||
*/
|
||||
bool Push(uint32_t value)
|
||||
{
|
||||
const uint32_t head = head_.load(std::memory_order_relaxed);
|
||||
const uint32_t next = Increment(head);
|
||||
|
||||
// Buffer is full if advancing head would collide with the consumer's tail.
|
||||
if (next == tail_.load(std::memory_order_acquire)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
slot_array()[head] = value;
|
||||
head_.store(next, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Consumer side: dequeue an index into `out`. Returns false if the buffer is empty.
|
||||
*/
|
||||
bool Pop(uint32_t *out)
|
||||
{
|
||||
const uint32_t tail = tail_.load(std::memory_order_relaxed);
|
||||
|
||||
// Buffer is empty if the consumer has caught up to the producer.
|
||||
if (tail == head_.load(std::memory_order_acquire)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*out = slot_array()[tail];
|
||||
tail_.store(Increment(tail), std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Approximate number of entries currently queued.
|
||||
*
|
||||
* Safe to call from either side, but the value may be stale the instant it returns. Intended for
|
||||
* metrics/backpressure heuristics, not for correctness decisions.
|
||||
*/
|
||||
uint32_t SizeApprox() const
|
||||
{
|
||||
const uint32_t head = head_.load(std::memory_order_acquire);
|
||||
const uint32_t tail = tail_.load(std::memory_order_acquire);
|
||||
return (head + capacity_ - tail) % capacity_;
|
||||
}
|
||||
|
||||
bool IsEmptyApprox() const
|
||||
{
|
||||
return head_.load(std::memory_order_acquire) ==
|
||||
tail_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
uint32_t capacity() const
|
||||
{
|
||||
return capacity_;
|
||||
}
|
||||
|
||||
private:
|
||||
uint32_t Increment(uint32_t index) const
|
||||
{
|
||||
// capacity_ is small and this avoids requiring a power-of-two capacity.
|
||||
return (index + 1) % capacity_;
|
||||
}
|
||||
|
||||
// The index slot array is allocated immediately after this struct in the same contiguous block.
|
||||
// (Named slot_array() rather than slots() to avoid Qt's `slots` keyword macro.)
|
||||
uint32_t *slot_array()
|
||||
{
|
||||
return reinterpret_cast<uint32_t *>(this + 1);
|
||||
}
|
||||
const uint32_t *slot_array() const
|
||||
{
|
||||
return reinterpret_cast<const uint32_t *>(this + 1);
|
||||
}
|
||||
|
||||
// Producer writes head_, consumer writes tail_. Kept on separate cache lines would be ideal, but
|
||||
// since these live in shared memory with a trailing flexible array we keep the header compact and
|
||||
// rely on acquire/release ordering for correctness.
|
||||
std::atomic<uint32_t> head_;
|
||||
std::atomic<uint32_t> tail_;
|
||||
uint32_t capacity_;
|
||||
|
||||
static_assert(sizeof(std::atomic<uint32_t>) == sizeof(uint32_t),
|
||||
"atomic<uint32_t> must be lock-free POD-sized for shared memory use");
|
||||
};
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace olive
|
||||
|
||||
#endif // IPC_SPSCRINGBUFFER_H
|
||||
@@ -0,0 +1,437 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
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/>.
|
||||
|
||||
***/
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include <QFile>
|
||||
#include <QGuiApplication>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QMatrix4x4>
|
||||
#include <QOpenGLContext>
|
||||
#include <QSurfaceFormat>
|
||||
|
||||
#include "common/qtutils.h"
|
||||
#include "config/config.h"
|
||||
#include "node/factory.h"
|
||||
#include "node/input/multicam/multicamnode.h"
|
||||
#include "node/project/serializer/serializer.h"
|
||||
#include "render/ipc/frameslotpool.h"
|
||||
#include "render/ipc/ipcmessage.h"
|
||||
#include "render/ipc/sharedmemoryregion.h"
|
||||
#include "render/opengl/openglrenderer.h"
|
||||
#include "render/rendermanager.h"
|
||||
#include "render/renderprocessor.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr int kProtocolVersion = 1;
|
||||
constexpr int kDefaultWidth = 1920;
|
||||
constexpr int kDefaultHeight = 1080;
|
||||
constexpr int kDefaultFrameRate = 24;
|
||||
|
||||
void InstallSurfaceFormat()
|
||||
{
|
||||
QSurfaceFormat format;
|
||||
format.setVersion(3, 2);
|
||||
format.setProfile(QSurfaceFormat::CoreProfile);
|
||||
format.setDepthBufferSize(24);
|
||||
QSurfaceFormat::setDefaultFormat(format);
|
||||
}
|
||||
|
||||
void LogError(const QString &message)
|
||||
{
|
||||
const QByteArray line = QByteArray("worker: ") + message.toUtf8() + '\n';
|
||||
fwrite(line.constData(), 1, size_t(line.size()), stderr);
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
QJsonObject ErrorMessage(const QString &message, qint64 ticket_id = 0)
|
||||
{
|
||||
QJsonObject o;
|
||||
o["type"] = olive::ipc::msgtype::kError;
|
||||
o["message"] = message;
|
||||
if (ticket_id) {
|
||||
o["ticket"] = double(ticket_id);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
class RenderWorker {
|
||||
public:
|
||||
RenderWorker(olive::OpenGLRenderer *renderer, QFile *out)
|
||||
: renderer_(renderer)
|
||||
, out_(out)
|
||||
{
|
||||
}
|
||||
|
||||
~RenderWorker()
|
||||
{
|
||||
project_.reset();
|
||||
olive::ProjectSerializer::Destroy();
|
||||
olive::NodeFactory::Destroy();
|
||||
}
|
||||
|
||||
bool InitializeRuntime()
|
||||
{
|
||||
olive::Config::Load();
|
||||
olive::NodeFactory::Initialize();
|
||||
olive::ColorManager::SetUpDefaultConfig();
|
||||
olive::ProjectSerializer::Initialize();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SendStartupHandshake()
|
||||
{
|
||||
olive::ipc::HandshakeMsg hs;
|
||||
hs.protocol_version = kProtocolVersion;
|
||||
hs.shm_key = QString();
|
||||
hs.input_slots = 0;
|
||||
hs.output_slots = 0;
|
||||
hs.slot_data_bytes = 0;
|
||||
|
||||
QJsonObject handshake = hs.ToJson();
|
||||
if (QOpenGLContext *ctx = renderer_->context()) {
|
||||
const QSurfaceFormat fmt = ctx->format();
|
||||
handshake["gl_major"] = fmt.majorVersion();
|
||||
handshake["gl_minor"] = fmt.minorVersion();
|
||||
}
|
||||
|
||||
return Write(handshake);
|
||||
}
|
||||
|
||||
bool Handle(const QJsonObject &message)
|
||||
{
|
||||
const QString type = message["type"].toString();
|
||||
|
||||
if (type == QLatin1String(olive::ipc::msgtype::kHandshake)) {
|
||||
olive::ipc::HandshakeMsg hs;
|
||||
if (!olive::ipc::HandshakeMsg::FromJson(message, &hs)) {
|
||||
return Write(ErrorMessage(QStringLiteral("invalid handshake message")));
|
||||
}
|
||||
return AttachOutputPool(hs);
|
||||
}
|
||||
|
||||
if (type == QLatin1String(olive::ipc::msgtype::kLoadGraph)) {
|
||||
olive::ipc::LoadGraphMsg load;
|
||||
if (!olive::ipc::LoadGraphMsg::FromJson(message, &load)) {
|
||||
return Write(ErrorMessage(QStringLiteral("invalid load_graph message")));
|
||||
}
|
||||
return LoadGraph(load.path);
|
||||
}
|
||||
|
||||
if (type == QLatin1String(olive::ipc::msgtype::kRenderFrame)) {
|
||||
olive::ipc::RenderFrameMsg render;
|
||||
if (!olive::ipc::RenderFrameMsg::FromJson(message, &render)) {
|
||||
return Write(ErrorMessage(QStringLiteral("invalid render_frame message")));
|
||||
}
|
||||
return RenderFrame(render);
|
||||
}
|
||||
|
||||
if (type == QLatin1String(olive::ipc::msgtype::kCancel)) {
|
||||
// Stage 5 wires cancellation into in-flight jobs. Stage 2 has only synchronous single-frame work.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type == QLatin1String(olive::ipc::msgtype::kShutdown)) {
|
||||
shutdown_requested_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return Write(ErrorMessage(QStringLiteral("unknown message type: %1").arg(type)));
|
||||
}
|
||||
|
||||
bool shutdown_requested() const
|
||||
{
|
||||
return shutdown_requested_;
|
||||
}
|
||||
|
||||
private:
|
||||
bool Write(const QJsonObject &message)
|
||||
{
|
||||
const bool ok = olive::ipc::WriteMessage(out_, message);
|
||||
out_->flush();
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool AttachOutputPool(const olive::ipc::HandshakeMsg &hs)
|
||||
{
|
||||
if (hs.protocol_version != kProtocolVersion) {
|
||||
return Write(ErrorMessage(QStringLiteral("unsupported protocol version %1")
|
||||
.arg(hs.protocol_version)));
|
||||
}
|
||||
|
||||
if (hs.shm_key.isEmpty() || hs.output_slots <= 0 || hs.slot_data_bytes <= 0) {
|
||||
return Write(ErrorMessage(QStringLiteral("handshake missing output shared-memory geometry")));
|
||||
}
|
||||
|
||||
const size_t bytes = olive::ipc::FrameSlotPool::BytesNeeded(
|
||||
uint32_t(hs.output_slots), size_t(hs.slot_data_bytes));
|
||||
if (!output_region_.Open(hs.shm_key, bytes, olive::ipc::SharedMemoryRegion::kAttach)) {
|
||||
return Write(ErrorMessage(QStringLiteral("failed to attach shared memory: %1")
|
||||
.arg(output_region_.error())));
|
||||
}
|
||||
|
||||
output_pool_ = olive::ipc::FrameSlotPool::Attach(output_region_.data());
|
||||
if (!output_pool_->IsValid()) {
|
||||
output_region_.Close();
|
||||
output_pool_.reset();
|
||||
return Write(ErrorMessage(QStringLiteral("shared memory does not contain a frame slot pool")));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LoadGraph(const QString &path)
|
||||
{
|
||||
auto loaded = std::make_unique<olive::Project>();
|
||||
loaded->Initialize();
|
||||
|
||||
olive::ProjectSerializer::Result result =
|
||||
olive::ProjectSerializer::Load(loaded.get(), path, olive::ProjectSerializer::kProject);
|
||||
if (result != olive::ProjectSerializer::kSuccess) {
|
||||
return Write(ErrorMessage(QStringLiteral("failed to load graph %1: %2")
|
||||
.arg(path, result.GetDetails())));
|
||||
}
|
||||
|
||||
project_ = std::move(loaded);
|
||||
node_by_token_.clear();
|
||||
|
||||
const auto &data = result.GetLoadData();
|
||||
for (auto it = data.node_ptrs.cbegin(); it != data.node_ptrs.cend(); ++it) {
|
||||
node_by_token_.insert(QString::number(it.key()), it.value());
|
||||
}
|
||||
for (auto it = data.node_uuids.cbegin(); it != data.node_uuids.cend(); ++it) {
|
||||
node_by_token_.insert(it.value().toString(), it.key());
|
||||
node_by_token_.insert(it.value().toString(QUuid::WithoutBraces), it.key());
|
||||
}
|
||||
|
||||
QJsonObject ack;
|
||||
ack["type"] = QStringLiteral("graph_loaded");
|
||||
ack["nodes"] = node_by_token_.size();
|
||||
return Write(ack);
|
||||
}
|
||||
|
||||
olive::Node *FindNode(const QString &token) const
|
||||
{
|
||||
if (olive::Node *node = node_by_token_.value(token, nullptr)) {
|
||||
return node;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
const quintptr ptr = token.toULongLong(&ok, 0);
|
||||
if (ok) {
|
||||
return node_by_token_.value(QString::number(ptr), nullptr);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool RenderFrame(const olive::ipc::RenderFrameMsg &message)
|
||||
{
|
||||
if (!project_) {
|
||||
return Write(ErrorMessage(QStringLiteral("render_frame received before load_graph"),
|
||||
message.ticket_id));
|
||||
}
|
||||
if (!output_pool_ || !output_pool_->IsValid()) {
|
||||
return Write(ErrorMessage(QStringLiteral("render_frame received before output shm handshake"),
|
||||
message.ticket_id));
|
||||
}
|
||||
|
||||
olive::Node *node = FindNode(message.node_uuid);
|
||||
if (!node) {
|
||||
return Write(ErrorMessage(QStringLiteral("render node not found: %1").arg(message.node_uuid),
|
||||
message.ticket_id));
|
||||
}
|
||||
|
||||
olive::VideoParams vparams(message.width > 0 ? message.width : kDefaultWidth,
|
||||
message.height > 0 ? message.height : kDefaultHeight,
|
||||
olive::rational(1, kDefaultFrameRate),
|
||||
message.format >= 0
|
||||
? olive::PixelFormat::Format(message.format)
|
||||
: olive::PixelFormat::F32,
|
||||
message.channel_count > 0
|
||||
? message.channel_count
|
||||
: olive::VideoParams::kRGBAChannelCount);
|
||||
|
||||
olive::RenderTicketPtr ticket = std::make_shared<olive::RenderTicket>();
|
||||
ticket->setProperty("node", olive::QtUtils::PtrToValue(node));
|
||||
ticket->setProperty("time", QVariant::fromValue(
|
||||
olive::rational(int(message.time_num), int(message.time_den))));
|
||||
ticket->setProperty("size", QSize(message.width, message.height));
|
||||
ticket->setProperty("matrix", QMatrix4x4());
|
||||
ticket->setProperty("format",
|
||||
message.format >= 0
|
||||
? olive::PixelFormat::Format(message.format)
|
||||
: olive::PixelFormat::INVALID);
|
||||
ticket->setProperty("usecache", false);
|
||||
ticket->setProperty("channelcount", message.channel_count);
|
||||
ticket->setProperty("mode", olive::RenderMode::Mode(message.mode));
|
||||
ticket->setProperty("type", olive::RenderManager::kTypeVideo);
|
||||
ticket->setProperty("colormanager", olive::QtUtils::PtrToValue(project_->color_manager()));
|
||||
ticket->setProperty("coloroutput", QVariant::fromValue(olive::ColorProcessorPtr()));
|
||||
ticket->setProperty("vparam", QVariant::fromValue(vparams));
|
||||
ticket->setProperty("aparam", QVariant::fromValue(olive::AudioParams()));
|
||||
ticket->setProperty("return", olive::RenderManager::kFrame);
|
||||
ticket->setProperty("cache", QString());
|
||||
ticket->setProperty("cachetimebase", QVariant::fromValue(olive::rational(1)));
|
||||
ticket->setProperty("cacheid", QVariant::fromValue(QUuid()));
|
||||
ticket->setProperty("multicam", olive::QtUtils::PtrToValue(static_cast<void *>(nullptr)));
|
||||
|
||||
ticket->Start();
|
||||
olive::RenderProcessor::Process(ticket, renderer_, nullptr, &shader_cache_);
|
||||
if (!ticket->HasResult()) {
|
||||
return Write(ErrorMessage(QStringLiteral("render produced no frame"), message.ticket_id));
|
||||
}
|
||||
|
||||
olive::FramePtr frame = ticket->Get().value<olive::FramePtr>();
|
||||
if (!frame || !frame->is_allocated()) {
|
||||
return Write(ErrorMessage(QStringLiteral("render result was empty"), message.ticket_id));
|
||||
}
|
||||
|
||||
uint32_t slot = 0;
|
||||
if (!output_pool_->Acquire(&slot)) {
|
||||
return Write(ErrorMessage(QStringLiteral("no free output frame slot"), message.ticket_id));
|
||||
}
|
||||
|
||||
const int data_size = frame->allocated_size();
|
||||
if (data_size > int(output_pool_->slot_data_bytes())) {
|
||||
output_pool_->Release(slot);
|
||||
return Write(ErrorMessage(QStringLiteral("rendered frame does not fit output slot"),
|
||||
message.ticket_id));
|
||||
}
|
||||
|
||||
std::memcpy(output_pool_->SlotData(slot), frame->const_data(), size_t(data_size));
|
||||
olive::ipc::FrameSlotMeta *meta = output_pool_->Meta(slot);
|
||||
meta->id = message.ticket_id;
|
||||
meta->time_num = frame->timestamp().numerator();
|
||||
meta->time_den = frame->timestamp().denominator();
|
||||
meta->width = frame->width();
|
||||
meta->height = frame->height();
|
||||
meta->format = int32_t(frame->format());
|
||||
meta->channel_count = frame->channel_count();
|
||||
meta->linesize = frame->linesize_bytes();
|
||||
meta->data_size = data_size;
|
||||
|
||||
if (!output_pool_->Publish(slot)) {
|
||||
output_pool_->Release(slot);
|
||||
return Write(ErrorMessage(QStringLiteral("failed to publish output frame slot"),
|
||||
message.ticket_id));
|
||||
}
|
||||
|
||||
olive::ipc::FrameReadyMsg ready;
|
||||
ready.ticket_id = message.ticket_id;
|
||||
ready.output_slot = int(slot);
|
||||
return Write(ready.ToJson());
|
||||
}
|
||||
|
||||
olive::OpenGLRenderer *renderer_;
|
||||
QFile *out_;
|
||||
bool shutdown_requested_ = false;
|
||||
std::unique_ptr<olive::Project> project_;
|
||||
QHash<QString, olive::Node *> node_by_token_;
|
||||
olive::ipc::SharedMemoryRegion output_region_;
|
||||
std::optional<olive::ipc::FrameSlotPool> output_pool_;
|
||||
olive::ShaderCache shader_cache_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL);
|
||||
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
|
||||
InstallSurfaceFormat();
|
||||
|
||||
QGuiApplication app(argc, argv);
|
||||
QCoreApplication::setOrganizationName(QStringLiteral("oakvideoeditor.org"));
|
||||
QCoreApplication::setApplicationName(QStringLiteral("olive-render-worker"));
|
||||
|
||||
QFile in;
|
||||
QFile out;
|
||||
if (!in.open(stdin, QIODevice::ReadOnly | QIODevice::Unbuffered) ||
|
||||
!out.open(stdout, QIODevice::WriteOnly | QIODevice::Unbuffered)) {
|
||||
LogError(QStringLiteral("failed to open stdio control pipes"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto *renderer = new olive::OpenGLRenderer();
|
||||
if (!renderer->Init()) {
|
||||
LogError(QStringLiteral("failed to initialize OpenGL renderer"));
|
||||
delete renderer;
|
||||
return 1;
|
||||
}
|
||||
renderer->PostInit();
|
||||
|
||||
QOpenGLContext *ctx = renderer->context();
|
||||
if (!ctx || !ctx->isValid()) {
|
||||
LogError(QStringLiteral("OpenGL context is not valid after init"));
|
||||
renderer->Destroy();
|
||||
renderer->PostDestroy();
|
||||
delete renderer;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int exit_code = 0;
|
||||
{
|
||||
RenderWorker worker(renderer, &out);
|
||||
if (!worker.InitializeRuntime() || !worker.SendStartupHandshake()) {
|
||||
exit_code = 1;
|
||||
} else {
|
||||
QByteArray buffer;
|
||||
while (!worker.shutdown_requested() && !in.atEnd()) {
|
||||
const QByteArray chunk = in.readLine();
|
||||
if (chunk.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
buffer.append(chunk);
|
||||
while (true) {
|
||||
QJsonObject message;
|
||||
bool ok = true;
|
||||
if (!olive::ipc::ReadMessage(&buffer, &message, &ok)) {
|
||||
if (!ok) {
|
||||
olive::ipc::WriteMessage(
|
||||
&out, ErrorMessage(QStringLiteral("malformed control message")));
|
||||
out.flush();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!worker.Handle(message)) {
|
||||
exit_code = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderer->Destroy();
|
||||
renderer->PostDestroy();
|
||||
delete renderer;
|
||||
|
||||
return exit_code;
|
||||
}
|
||||
Reference in New Issue
Block a user