remove shifts, operations, and make caches node-agnostic
This commit is contained in:
@@ -170,108 +170,6 @@ void AudioPlaybackCache::WriteSilence(const TimeRange &range)
|
||||
WritePCM(range, {range}, SampleBuffer());
|
||||
}
|
||||
|
||||
void AudioPlaybackCache::ShiftEvent(const rational &from_in_time, const rational &to_in_time)
|
||||
{
|
||||
visual_.Shift(from_in_time, to_in_time);
|
||||
|
||||
qint64 to = params_.time_to_bytes_per_channel(to_in_time);
|
||||
qint64 from = params_.time_to_bytes_per_channel(from_in_time);
|
||||
|
||||
if (from >= playlist_.GetLength()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int to_seg_index = playlist_.GetIndexOfPosition(to);
|
||||
int from_seg_index = playlist_.GetIndexOfPosition(from);
|
||||
|
||||
qint64 from_seg_start = playlist_.at(from_seg_index).offset();
|
||||
qint64 from_seg_end = playlist_.at(from_seg_index).end();
|
||||
|
||||
if (from < to) {
|
||||
// Shifting forwards, we must insert a new region and split a segment in half if necessary
|
||||
int insert_index;
|
||||
|
||||
// Determine at what part of the array we'll be insert into
|
||||
if (from == from_seg_start) {
|
||||
insert_index = from_seg_index;
|
||||
} else {
|
||||
insert_index = from_seg_index + 1;
|
||||
|
||||
if (from < from_seg_end) {
|
||||
// Split from segment into two
|
||||
Segment second = CloneSegment(playlist_.at(from_seg_index));
|
||||
|
||||
TrimSegmentOut(&playlist_[from_seg_index], from - from_seg_start);
|
||||
TrimSegmentIn(&second, from_seg_end - from);
|
||||
|
||||
playlist_.insert(insert_index, second);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert silent segments
|
||||
qint64 time_to_insert = to - from;
|
||||
|
||||
while (time_to_insert) {
|
||||
qint64 new_seg_sz = qMin(kDefaultSegmentSizePerChannel, time_to_insert);
|
||||
|
||||
// Set offset to 0 for now and fill it in later
|
||||
playlist_.insert(insert_index, CreateSegment(new_seg_sz, 0));
|
||||
|
||||
time_to_insert -= new_seg_sz;
|
||||
}
|
||||
|
||||
UpdateOffsetsFrom(insert_index);
|
||||
|
||||
} else {
|
||||
// Shifting backwards, we'll be removing segments and truncating them if necessary
|
||||
qint64 to_seg_start = playlist_.at(to_seg_index).offset();
|
||||
qint64 to_seg_end = playlist_.at(to_seg_index).end();
|
||||
|
||||
if (to_seg_index == from_seg_index) {
|
||||
if (to > to_seg_start && from < from_seg_end) {
|
||||
// Duplicate segment into two segments and trim each side to move the space in between
|
||||
Segment second = CloneSegment(playlist_.at(to_seg_index));
|
||||
from_seg_index++;
|
||||
playlist_.insert(from_seg_index, second);
|
||||
|
||||
TrimSegmentOut(&playlist_[to_seg_index], to - to_seg_start);
|
||||
TrimSegmentIn(&playlist_[from_seg_index], to_seg_end - from);
|
||||
} else if (to == to_seg_start && from == from_seg_end) {
|
||||
// Segment contains entire shift area, just remove it
|
||||
RemoveSegmentFromArray(to_seg_index);
|
||||
} else if (to == to_seg_start) {
|
||||
// Trim segment in
|
||||
TrimSegmentIn(&playlist_[to_seg_index], to_seg_end - from);
|
||||
} else {
|
||||
// Assume from == to_seg_end
|
||||
TrimSegmentOut(&playlist_[to_seg_index], to_seg_end - to);
|
||||
}
|
||||
} else {
|
||||
// Remove any segments between if there are any
|
||||
while (from_seg_index != to_seg_index+1) {
|
||||
RemoveSegmentFromArray(to_seg_index+1);
|
||||
from_seg_index--;
|
||||
}
|
||||
|
||||
if (from == from_seg_end) {
|
||||
RemoveSegmentFromArray(from_seg_index);
|
||||
} else {
|
||||
// Assume from > from_seg_start and trim its in point
|
||||
TrimSegmentIn(&playlist_[from_seg_index], from_seg_end - from);
|
||||
}
|
||||
|
||||
if (to == to_seg_start) {
|
||||
// Assume from >= to_seg_end, remove "to" segment
|
||||
RemoveSegmentFromArray(to_seg_index);
|
||||
} else {
|
||||
TrimSegmentOut(&playlist_[to_seg_index], to_seg_end - to);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateOffsetsFrom(to_seg_index);
|
||||
}
|
||||
}
|
||||
|
||||
AudioPlaybackCache::Segment AudioPlaybackCache::CloneSegment(const AudioPlaybackCache::Segment &s) const
|
||||
{
|
||||
Segment new_seg = s;
|
||||
@@ -318,9 +216,11 @@ QString AudioPlaybackCache::GenerateSegmentFilename() const
|
||||
{
|
||||
QString new_seg_filename;
|
||||
|
||||
QDir cache_dir(QDir(GetCacheDirectory()).filePath(GetUuid().toString()));
|
||||
|
||||
do {
|
||||
uint32_t r = QRandomGenerator::global()->generate();
|
||||
new_seg_filename = QDir(GetCacheDirectory()).filePath(QStringLiteral("%1.pcm").arg(r));
|
||||
new_seg_filename = cache_dir.filePath(QStringLiteral("%1.pcm").arg(r));
|
||||
} while (QFileInfo::exists(new_seg_filename));
|
||||
|
||||
return new_seg_filename;
|
||||
@@ -402,7 +302,7 @@ AudioPlaybackCache::PlaybackDevice *AudioPlaybackCache::CreatePlaybackDevice(QOb
|
||||
PlaybackDevice *d = new PlaybackDevice(playlist_, params_.bytes_per_sample_per_channel(), parent);
|
||||
|
||||
// If we're child of a viewer, set the data limit so audio doesn't play beyond the length
|
||||
if (ViewerOutput *viewer = viewer_parent()) {
|
||||
if (ViewerOutput *viewer = dynamic_cast<ViewerOutput*>(this->parent())) {
|
||||
d->SetDataLimit(params_.time_to_bytes_per_channel(viewer->GetAudioLength()));
|
||||
}
|
||||
|
||||
|
||||
@@ -208,9 +208,6 @@ public:
|
||||
signals:
|
||||
void ParametersChanged();
|
||||
|
||||
protected:
|
||||
virtual void ShiftEvent(const rational& from, const rational& to) override;
|
||||
|
||||
private:
|
||||
static const qint64 kDefaultSegmentSizePerChannel;
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ void FrameHashCache::ValidateTime(const rational &time)
|
||||
|
||||
bool FrameHashCache::SaveCacheFrame(const int64_t &time, FramePtr frame) const
|
||||
{
|
||||
return SaveCacheFrame(GetCacheDirectory(), uuid_, time, frame);
|
||||
return SaveCacheFrame(GetCacheDirectory(), GetUuid(), time, frame);
|
||||
}
|
||||
|
||||
bool FrameHashCache::SaveCacheFrame(const QString &cache_path, const QUuid &uuid, const int64_t &time, FramePtr frame)
|
||||
@@ -125,7 +125,7 @@ FramePtr FrameHashCache::LoadCacheFrame(const QString &cache_path, const QUuid &
|
||||
|
||||
FramePtr FrameHashCache::LoadCacheFrame(const int64_t &hash) const
|
||||
{
|
||||
return LoadCacheFrame(GetCacheDirectory(), uuid_, hash);
|
||||
return LoadCacheFrame(GetCacheDirectory(), GetUuid(), hash);
|
||||
}
|
||||
|
||||
FramePtr FrameHashCache::LoadCacheFrame(const QString &fn)
|
||||
@@ -212,7 +212,7 @@ void FrameHashCache::HashDeleted(const QString& path, const QString &filename)
|
||||
}
|
||||
|
||||
QFileInfo info(filename);
|
||||
if (uuid_.toString() != info.dir().dirName()) {
|
||||
if (GetUuid().toString() != info.dir().dirName()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -229,12 +229,12 @@ void FrameHashCache::ProjectInvalidated(Project *p)
|
||||
|
||||
QString FrameHashCache::CachePathName(const int64_t &time) const
|
||||
{
|
||||
return CachePathName(GetCacheDirectory(), uuid_, time);
|
||||
return CachePathName(GetCacheDirectory(), GetUuid(), time);
|
||||
}
|
||||
|
||||
QString FrameHashCache::CachePathName(const rational &time) const
|
||||
{
|
||||
return CachePathName(GetCacheDirectory(), uuid_, time, timebase_);
|
||||
return CachePathName(GetCacheDirectory(), GetUuid(), time, timebase_);
|
||||
}
|
||||
|
||||
QString FrameHashCache::CachePathName(const QString &cache_path, const QUuid &cache_id, const int64_t &time)
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
#ifndef VIDEORENDERFRAMECACHE_H
|
||||
#define VIDEORENDERFRAMECACHE_H
|
||||
|
||||
#include <QMutex>
|
||||
#include <QUuid>
|
||||
|
||||
#include "common/rational.h"
|
||||
#include "common/timecodefunctions.h"
|
||||
#include "common/timerange.h"
|
||||
@@ -39,9 +36,6 @@ class FrameHashCache : public PlaybackCache
|
||||
public:
|
||||
FrameHashCache(QObject* parent = nullptr);
|
||||
|
||||
const QUuid &GetUuid() const { return uuid_; }
|
||||
void SetUuid(const QUuid &u) { uuid_ = u; }
|
||||
|
||||
const rational &GetTimebase() const { return timebase_; }
|
||||
|
||||
void SetTimebase(const rational& tb);
|
||||
@@ -86,8 +80,6 @@ private:
|
||||
|
||||
rational timebase_;
|
||||
|
||||
QUuid uuid_;
|
||||
|
||||
static const QString kCacheFormatExtension;
|
||||
|
||||
private slots:
|
||||
|
||||
@@ -43,40 +43,16 @@ void PlaybackCache::Invalidate(const TimeRange &r, bool signal)
|
||||
}
|
||||
}
|
||||
|
||||
Node *PlaybackCache::parent() const
|
||||
{
|
||||
return dynamic_cast<Node*>(QObject::parent());
|
||||
}
|
||||
|
||||
void PlaybackCache::InvalidateAll()
|
||||
{
|
||||
Invalidate(TimeRange(0, RATIONAL_MAX));
|
||||
}
|
||||
|
||||
void PlaybackCache::Shift(rational from, rational to)
|
||||
{
|
||||
if (from == to) {
|
||||
return;
|
||||
}
|
||||
|
||||
// An region between `from` and `to` will be inserted or spliced out
|
||||
TimeRangeList ranges_to_shift = validated_.Intersects(TimeRange(from, RATIONAL_MAX));
|
||||
|
||||
// Remove all ranges starting at to
|
||||
validated_.remove(TimeRange(qMin(from, to), RATIONAL_MAX));
|
||||
|
||||
// Restore ranges shifted
|
||||
rational diff = to - from;
|
||||
foreach (const TimeRange& r, ranges_to_shift) {
|
||||
validated_.insert(r + diff);
|
||||
}
|
||||
|
||||
// Tell derivatives that a shift has occurred
|
||||
ShiftEvent(from, to);
|
||||
|
||||
// Emit signals
|
||||
emit Shifted(from, to);
|
||||
|
||||
if (diff > 0) {
|
||||
//emit Invalidated(TimeRange(from, to));
|
||||
}
|
||||
}
|
||||
|
||||
void PlaybackCache::Validate(const TimeRange &r, bool signal)
|
||||
{
|
||||
validated_.insert(r);
|
||||
@@ -90,15 +66,17 @@ void PlaybackCache::InvalidateEvent(const TimeRange &)
|
||||
{
|
||||
}
|
||||
|
||||
void PlaybackCache::ShiftEvent(const rational &, const rational &)
|
||||
{
|
||||
}
|
||||
|
||||
Project *PlaybackCache::GetProject() const
|
||||
{
|
||||
return Project::GetProjectFromObject(this);
|
||||
}
|
||||
|
||||
PlaybackCache::PlaybackCache(QObject *parent) :
|
||||
QObject(parent)
|
||||
{
|
||||
uuid_ = QUuid::createUuid();
|
||||
}
|
||||
|
||||
TimeRangeList PlaybackCache::GetInvalidatedRanges(TimeRange intersecting)
|
||||
{
|
||||
TimeRangeList invalidated;
|
||||
@@ -133,9 +111,4 @@ QString PlaybackCache::GetCacheDirectory() const
|
||||
}
|
||||
}
|
||||
|
||||
ViewerOutput *PlaybackCache::viewer_parent() const
|
||||
{
|
||||
return dynamic_cast<ViewerOutput*>(parent());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+10
-13
@@ -21,14 +21,15 @@
|
||||
#ifndef PLAYBACKCACHE_H
|
||||
#define PLAYBACKCACHE_H
|
||||
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
#include <QUuid>
|
||||
|
||||
#include "common/jobtime.h"
|
||||
#include "common/timerange.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
class Node;
|
||||
class Project;
|
||||
class ViewerOutput;
|
||||
|
||||
@@ -36,10 +37,10 @@ class PlaybackCache : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
PlaybackCache(QObject* parent = nullptr) :
|
||||
QObject(parent)
|
||||
{
|
||||
}
|
||||
PlaybackCache(QObject* parent = nullptr);
|
||||
|
||||
const QUuid &GetUuid() const { return uuid_; }
|
||||
void SetUuid(const QUuid &u) { uuid_ = u; }
|
||||
|
||||
TimeRangeList GetInvalidatedRanges(TimeRange intersecting);
|
||||
TimeRangeList GetInvalidatedRanges(const rational &length)
|
||||
@@ -55,36 +56,32 @@ public:
|
||||
|
||||
QString GetCacheDirectory() const;
|
||||
|
||||
ViewerOutput *viewer_parent() const;
|
||||
|
||||
void Invalidate(const TimeRange& r, bool signal = true);
|
||||
|
||||
const TimeRangeList &GetValidatedRanges() const { return validated_; }
|
||||
|
||||
Node *parent() const;
|
||||
|
||||
public slots:
|
||||
void InvalidateAll();
|
||||
|
||||
void Shift(rational from, rational to);
|
||||
|
||||
signals:
|
||||
void Invalidated(const olive::TimeRange& r);
|
||||
|
||||
void Validated(const olive::TimeRange& r);
|
||||
|
||||
void Shifted(const olive::rational& from, const olive::rational& to);
|
||||
|
||||
protected:
|
||||
void Validate(const TimeRange& r, bool signal = true);
|
||||
|
||||
virtual void InvalidateEvent(const TimeRange& range);
|
||||
|
||||
virtual void ShiftEvent(const rational& from, const rational& to);
|
||||
|
||||
Project* GetProject() const;
|
||||
|
||||
private:
|
||||
TimeRangeList validated_;
|
||||
|
||||
QUuid uuid_;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -557,7 +557,9 @@ RenderTicketWatcher* PreviewAutoCacher::RenderFrame(const rational& time, Render
|
||||
watcher->setProperty("job", QVariant::fromValue(last_update_time_));
|
||||
connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::VideoRendered);
|
||||
video_tasks_.insert(watcher, time);
|
||||
watcher->SetTicket(RenderManager::instance()->RenderFrame(copied_viewer_node_,
|
||||
watcher->SetTicket(RenderManager::instance()->RenderFrame(copied_viewer_node_->GetConnectedTextureOutput(),
|
||||
copied_viewer_node_->GetVideoParams(),
|
||||
copied_viewer_node_->GetAudioParams(),
|
||||
copied_color_manager_,
|
||||
time,
|
||||
RenderMode::kOffline,
|
||||
@@ -574,7 +576,7 @@ RenderTicketPtr PreviewAutoCacher::RenderAudio(const TimeRange &r, bool generate
|
||||
connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::AudioRendered);
|
||||
audio_tasks_.insert(watcher, r);
|
||||
|
||||
RenderTicketPtr ticket = RenderManager::instance()->RenderAudio(copied_viewer_node_, r, RenderMode::kOffline, generate_waveforms, priority);
|
||||
RenderTicketPtr ticket = RenderManager::instance()->RenderAudio(copied_viewer_node_->GetConnectedSampleOutput(), r, copied_viewer_node_->GetAudioParams(), RenderMode::kOffline, generate_waveforms, priority);
|
||||
watcher->SetTicket(ticket);
|
||||
return ticket;
|
||||
}
|
||||
|
||||
@@ -76,16 +76,16 @@ RenderManager::~RenderManager()
|
||||
}
|
||||
}
|
||||
|
||||
RenderTicketPtr RenderManager::RenderFrame(ViewerOutput *viewer, ColorManager* color_manager,
|
||||
const rational& time, RenderMode::Mode mode,
|
||||
RenderTicketPtr RenderManager::RenderFrame(Node *node, const VideoParams &vparam, const AudioParams ¶m,
|
||||
ColorManager* color_manager, const rational& time, RenderMode::Mode mode,
|
||||
FrameHashCache* cache, RenderTicketPriority priority, ReturnType return_type)
|
||||
{
|
||||
return RenderFrame(viewer,
|
||||
return RenderFrame(node,
|
||||
color_manager,
|
||||
time,
|
||||
mode,
|
||||
viewer->GetVideoParams(),
|
||||
viewer->GetAudioParams(),
|
||||
vparam,
|
||||
param,
|
||||
QSize(0, 0),
|
||||
QMatrix4x4(),
|
||||
VideoParams::kFormatInvalid,
|
||||
@@ -95,7 +95,7 @@ RenderTicketPtr RenderManager::RenderFrame(ViewerOutput *viewer, ColorManager* c
|
||||
return_type);
|
||||
}
|
||||
|
||||
RenderTicketPtr RenderManager::RenderFrame(ViewerOutput *viewer, ColorManager* color_manager,
|
||||
RenderTicketPtr RenderManager::RenderFrame(Node *node, ColorManager* color_manager,
|
||||
const rational& time, RenderMode::Mode mode,
|
||||
const VideoParams &video_params, const AudioParams &audio_params,
|
||||
const QSize& force_size,
|
||||
@@ -106,7 +106,7 @@ RenderTicketPtr RenderManager::RenderFrame(ViewerOutput *viewer, ColorManager* c
|
||||
// Create ticket
|
||||
RenderTicketPtr ticket = std::make_shared<RenderTicket>();
|
||||
|
||||
ticket->setProperty("viewer", Node::PtrToValue(viewer));
|
||||
ticket->setProperty("node", Node::PtrToValue(node));
|
||||
ticket->setProperty("time", QVariant::fromValue(time));
|
||||
ticket->setProperty("size", force_size);
|
||||
ticket->setProperty("matrix", force_matrix);
|
||||
@@ -130,17 +130,12 @@ RenderTicketPtr RenderManager::RenderFrame(ViewerOutput *viewer, ColorManager* c
|
||||
return ticket;
|
||||
}
|
||||
|
||||
RenderTicketPtr RenderManager::RenderAudio(ViewerOutput* viewer, const TimeRange& r, RenderMode::Mode mode, bool generate_waveforms, RenderTicketPriority priority)
|
||||
{
|
||||
return RenderAudio(viewer, r, viewer->GetAudioParams(), mode, generate_waveforms, priority);
|
||||
}
|
||||
|
||||
RenderTicketPtr RenderManager::RenderAudio(ViewerOutput* viewer, const TimeRange &r, const AudioParams ¶ms, RenderMode::Mode mode, bool generate_waveforms, RenderTicketPriority priority)
|
||||
RenderTicketPtr RenderManager::RenderAudio(Node *node, const TimeRange &r, const AudioParams ¶ms, RenderMode::Mode mode, bool generate_waveforms, RenderTicketPriority priority)
|
||||
{
|
||||
// Create ticket
|
||||
RenderTicketPtr ticket = std::make_shared<RenderTicket>();
|
||||
|
||||
ticket->setProperty("viewer", Node::PtrToValue(viewer));
|
||||
ticket->setProperty("node", Node::PtrToValue(node));
|
||||
ticket->setProperty("time", QVariant::fromValue(r));
|
||||
ticket->setProperty("type", kTypeAudio);
|
||||
ticket->setProperty("mode", mode);
|
||||
|
||||
@@ -76,10 +76,10 @@ public:
|
||||
*
|
||||
* This function is thread-safe.
|
||||
*/
|
||||
RenderTicketPtr RenderFrame(ViewerOutput *viewer, ColorManager* color_manager,
|
||||
RenderTicketPtr RenderFrame(Node *node, const VideoParams &vparam, const AudioParams ¶m, ColorManager* color_manager,
|
||||
const rational& time, RenderMode::Mode mode,
|
||||
FrameHashCache* cache = nullptr, RenderTicketPriority priority = RenderTicketPriority::kNormal, ReturnType return_type = kFrame);
|
||||
RenderTicketPtr RenderFrame(ViewerOutput* viewer, ColorManager* color_manager,
|
||||
RenderTicketPtr RenderFrame(Node *node, ColorManager* color_manager,
|
||||
const rational& time, RenderMode::Mode mode,
|
||||
const VideoParams& video_params, const AudioParams& audio_params,
|
||||
const QSize& force_size,
|
||||
@@ -94,8 +94,7 @@ public:
|
||||
*
|
||||
* This function is thread-safe.
|
||||
*/
|
||||
RenderTicketPtr RenderAudio(ViewerOutput* viewer, const TimeRange& r, const AudioParams& params, RenderMode::Mode mode, bool generate_waveforms, RenderTicketPriority priority = RenderTicketPriority::kNormal);
|
||||
RenderTicketPtr RenderAudio(ViewerOutput *viewer, const TimeRange& r, RenderMode::Mode mode, bool generate_waveforms, RenderTicketPriority priority = RenderTicketPriority::kNormal);
|
||||
RenderTicketPtr RenderAudio(Node *viewer, const TimeRange& r, const AudioParams& params, RenderMode::Mode mode, bool generate_waveforms, RenderTicketPriority priority = RenderTicketPriority::kNormal);
|
||||
|
||||
virtual void RunTicket(RenderTicketPtr ticket) const override;
|
||||
|
||||
|
||||
@@ -46,13 +46,12 @@ RenderProcessor::RenderProcessor(RenderTicketPtr ticket, Renderer *render_ctx, D
|
||||
|
||||
TexturePtr RenderProcessor::GenerateTexture(const rational &time, const rational &frame_length)
|
||||
{
|
||||
ViewerOutput* viewer = Node::ValueToPtr<ViewerOutput>(ticket_->property("viewer"));
|
||||
|
||||
TimeRange range = TimeRange(time, time + frame_length);
|
||||
|
||||
NodeValueTable table;
|
||||
if (Node *texture_output = viewer->GetConnectedTextureOutput()) {
|
||||
table = GenerateTable(texture_output, range);
|
||||
if (Node* node = Node::ValueToPtr<Node>(ticket_->property("node"))) {
|
||||
table = GenerateTable(node, range);
|
||||
}
|
||||
|
||||
NodeValue tex_val = table.Get(NodeValue::kTexture);
|
||||
@@ -205,12 +204,11 @@ void RenderProcessor::Run()
|
||||
}
|
||||
case RenderManager::kTypeAudio:
|
||||
{
|
||||
ViewerOutput* viewer = Node::ValueToPtr<ViewerOutput>(ticket_->property("viewer"));
|
||||
TimeRange time = ticket_->property("time").value<TimeRange>();
|
||||
|
||||
NodeValueTable table;
|
||||
if (Node *texture_output = viewer->GetConnectedSampleOutput()) {
|
||||
table = GenerateTable(texture_output, time);
|
||||
if (Node* node = Node::ValueToPtr<Node>(ticket_->property("node"))) {
|
||||
table = GenerateTable(node, time);
|
||||
}
|
||||
|
||||
NodeValue sample_val = table.Get(NodeValue::kSamples);
|
||||
|
||||
Reference in New Issue
Block a user