#include "videorenderworker.h" #include #include "common/define.h" #include "node/node.h" #include "render/pixelservice.h" VideoRenderWorker::VideoRenderWorker(DecoderCache *decoder_cache, VideoRenderFrameCache *frame_cache, QObject *parent) : RenderWorker(decoder_cache, parent), frame_cache_(frame_cache) { } const VideoRenderingParams &VideoRenderWorker::video_params() { return video_params_; } void VideoRenderWorker::RenderInternal(const NodeDependency& path) { // Get hash of node graph // We use SHA-1 for speed (benchmarks show it's the fastest hash available to us) QCryptographicHash hasher(QCryptographicHash::Sha1); HashNodeRecursively(&hasher, path.node()->parent(), path.in()); QByteArray hash = hasher.result(); if (frame_cache_->HasHash(hash)) { // We've already cached this hash, no need to continue emit HashAlreadyExists(path, hash); } else if (frame_cache_->TryCache(hash)) { // This hash is available for us to cache, start traversing graph RenderAsSibling(path); emit CompletedFrame(path, hash); } else { // Another thread must be caching this already, nothing to be done emit HashAlreadyBeingCached(); } } void VideoRenderWorker::HashNodeRecursively(QCryptographicHash *hash, Node* n, const rational& time) { // Resolve BlockList if (n->IsBlock() && (n = ValidateBlock(n, time)) == nullptr) { return; } // Add this Node's ID hash->addData(n->id().toUtf8()); foreach (NodeParam* param, n->parameters()) { // For each input, try to hash its value if (param->type() == NodeParam::kInput) { NodeInput* input = static_cast(param); // Get time adjustment TimeRange range = n->InputTimeAdjustment(input, TimeRange(time, time)); // For a single frame, we only care about one of the times rational input_time = range.in(); if (input->IsConnected()) { // Traverse down this edge HashNodeRecursively(hash, input->get_connected_node(), input_time); } else { // Grab the value at this time QVariant value = input->get_value_at_time(input_time); hash->addData(NodeParam::ValueToBytes(input->data_type(), value)); } // We have one exception for FOOTAGE types, since we resolve the footage into a frame in the renderer if (input->data_type() == NodeParam::kFootage) { StreamPtr stream = ResolveStreamFromInput(input); DecoderPtr decoder = ResolveDecoderFromInput(input); if (decoder != nullptr) { // Add footage details to hash // Footage filename hash->addData(stream->footage()->filename().toUtf8()); // Footage last modified date hash->addData(stream->footage()->timestamp().toString().toUtf8()); // Footage stream hash->addData(QString::number(stream->index()).toUtf8()); // Footage timestamp hash->addData(QString::number(decoder->GetTimestampFromTime(time)).toUtf8()); // FIXME: Add colorspace and alpha assoc } } } } } void VideoRenderWorker::SetParameters(const VideoRenderingParams &video_params) { video_params_ = video_params; ParametersChangedEvent(); } bool VideoRenderWorker::InitInternal() { download_buffer_.resize(PixelService::GetBufferSize(video_params().format(), video_params().effective_width(), video_params().effective_height())); return true; } void VideoRenderWorker::CloseInternal() { download_buffer_.clear(); } QList VideoRenderWorker::ProcessNodeInputsForTime(Node *n, const TimeRange &time) { QList connected_inputs; // Now we need to gather information about this Node's inputs foreach (NodeParam* param, n->parameters()) { // Check if this parameter is an input and if the Node is dependent on it if (param->type() == NodeParam::kInput) { NodeInput* input = static_cast(param); if (input->dependent()) { // If we're here, this input is necessary and we need to acquire the value for this Node if (input->IsConnected()) { // If it's connected to something, we need to retrieve that output at some point connected_inputs.append(input); } else { // If it isn't connected, it'll have the value we need inside it. We just need to store it for the node. input->set_stored_value(input->get_value_at_time(n->InputTimeAdjustment(input, time).in())); } // Special types like FOOTAGE require extra work from us (to decrease node complexity dealing with decoders) if (input->data_type() == NodeParam::kFootage) { input->set_stored_value(0); DecoderPtr decoder = ResolveDecoderFromInput(input); // By this point we should definitely have a decoder, and if we don't something's gone terribly wrong if (decoder != nullptr) { FramePtr frame = decoder->Retrieve(time.in()); if (frame != nullptr) { QVariant value = FrameToTexture(frame); input->set_stored_value(value); } } } } } } return connected_inputs; } void VideoRenderWorker::RenderAsSibling(NodeDependency dep) { NodeOutput* output = dep.node(); Node* original_node = output->parent(); Node* node; rational time = dep.in(); QList connected_inputs; QVariant value; // Set working state working_++; qDebug() << "Processing" << original_node->id() << original_node; original_node->LockProcessing(); // Firstly we check if this node is a "Block", if it is that means it's part of a linked list of mutually exclusive // nodes based on time and we might need to locate which Block to attach to if ((node = ValidateBlock(original_node, time)) == nullptr) { // ValidateBlock() may have returned nullptr if there was no Block found at this time so no texture to return output->cache_value(dep.range(), 0); original_node->UnlockProcessing(); goto end_render; } if (original_node != node) { // Ensure output is the output matching the node as it may have changed output = static_cast(node->GetParameterWithID(output->id())); // Switch locks original_node->UnlockProcessing(); node->LockProcessing(); qDebug() << "Deftly switched from" << original_node->id() << original_node << "to" << node->id() << node; } // Check if the output already has a value for this time if (output->has_cached_value(dep.range())) { // If so, we don't need to do anything, we can just send this value and exit here dep.node()->cache_value(dep.range(), output->get_cached_value(dep.range())); qDebug() << "Found a cached value on" << node->id() << output->id() << "at" << dep.range().in() << "-" << dep.range().out(); node->UnlockProcessing(); goto end_render; } // We need to run the Node's code to get the correct value for this time connected_inputs = ProcessNodeInputsForTime(node, dep.range()); // For each connected input, we need to acquire the value from another node while (!connected_inputs.isEmpty()) { // Remove any inputs from the list that we have valid cached values for already for (int i=0;iget_connected_output(); TimeRange input_time = node->InputTimeAdjustment(input, dep.range()); if (connected_output->has_cached_value(input_time)) { // This output already has this value, no need to process it again input->set_stored_value(connected_output->get_cached_value(input_time)); connected_inputs.removeAt(i); i--; } } // For every connected input except the first, we'll request another Node to do it int input_for_this_thread = -1; for (int i=0;iget_connected_node()->IsProcessingLocked()) { if (input_for_this_thread == -1) { // Store this later since we can process it on this thread as we wait for other threads input_for_this_thread = i; } else { TimeRange input_time = node->InputTimeAdjustment(input, dep.range()); emit RequestSibling(NodeDependency(input->get_connected_output(), input_time)); } } } if (input_for_this_thread > -1) { // In the mean time, this thread can go off to do the first parameter NodeInput* input = connected_inputs.at(input_for_this_thread); TimeRange input_range = node->InputTimeAdjustment(input, dep.range()); RenderAsSibling(NodeDependency(input->get_connected_output(), input_range)); input->set_stored_value(input->get_connected_output()->get_cached_value(input_range)); connected_inputs.removeAt(input_for_this_thread); } else { // Nothing for this thread to do. We'll wait 0.5 sec and check again for other nodes // FIXME: It would be nicer if this thread could do other nodes during this time QThread::msleep(500); } } // By this point, the node should have all the inputs it needs to render correctly // Check if we have a shader for this output if (OutputIsShader(output)) { // Run code value = RunNodeAsShader(output); } else { // Generate the value as expected value = node->Value(output); } // Place the value into the output output->cache_value(dep.range(), value); dep.node()->cache_value(dep.range(), value); // We're done! node->UnlockProcessing(); end_render: // End this working state working_--; } void VideoRenderWorker::Download(NodeDependency dep, QByteArray hash, QVariant texture, QString filename) { working_++; PixelFormatInfo format_info = PixelService::GetPixelFormatInfo(video_params().format()); // Set up OIIO::ImageSpec for compressing cached images on disk OIIO::ImageSpec spec(video_params().effective_width(), video_params().effective_height(), kRGBAChannels, format_info.oiio_desc); spec.attribute("compression", "dwaa:200"); TextureToBuffer(texture, download_buffer_); std::string working_fn_std = filename.toStdString(); std::unique_ptr out = OIIO::ImageOutput::create(working_fn_std); if (out) { qDebug() << "Saving to" << filename; out->open(working_fn_std, spec); out->write_image(format_info.oiio_desc, download_buffer_.data()); out->close(); emit CompletedDownload(dep, hash); } else { qWarning() << "Failed to open output file:" << filename; } working_--; }