Blob Blame Raw
/* === S Y N F I G ========================================================= */
/*!	\file renderer_canvas.cpp
**	\brief Template File
**
**	$Id$
**
**	\legal
**	Copyright (c) 2002-2005 Robert B. Quattlebaum Jr., Adrian Bentley
**	Copyright (c) 2007, 2008 Chris Moore
**  Copyright (c) 2011 Nikita Kitaev
**  ......... ... 2018 Ivan Mahonin
**
**	This package 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 2 of
**	the License, or (at your option) any later version.
**
**	This package 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.
**	\endlegal
*/
/* ========================================================================= */

/* === H E A D E R S ======================================================= */

#ifdef USING_PCH
#	include "pch.h"
#else
#ifdef HAVE_CONFIG_H
#	include <config.h>
#endif

#include <gdkmm/general.h>

#include <ETL/misc>

#include <synfig/general.h>
#include <synfig/canvas.h>
#include <synfig/context.h>
#include <synfig/threadpool.h>
#include <synfig/rendering/common/task/tasktransformation.h>
#include <synfig/rendering/renderer.h>

#include <gui/localization.h>
#include <gui/app.h>

#include "renderer_canvas.h"

#endif

/* === U S I N G =========================================================== */

using namespace std;
using namespace etl;
using namespace synfig;
using namespace studio;

/* === M A C R O S ========================================================= */

/* === G L O B A L S ======================================================= */

/* === P R O C E D U R E S ================================================= */

static int
int_floor(int x, int base)
{
	int m = x % base;
	return m < 0 ? x - base - m
		 : m > 0 ? x - m : x;
}

static int
int_ceil(int x, int base)
{
	int m = x % base;
	return m > 0 ? x + base - m
		 : m < 0 ? x - m : x;
}

/* === M E T H O D S ======================================================= */

Renderer_Canvas::Renderer_Canvas():
	refresh_id(), render_queued() { }

Renderer_Canvas::~Renderer_Canvas()
	{ cancel_render(); }

void
Renderer_Canvas::enqueue_rendering_task_callback(
	synfig::rendering::Renderer::Handle renderer,
	synfig::rendering::Task::Handle task,
	synfig::rendering::TaskEvent::Handle event )
{
	// Handles will protect objects from deletion before this call
	renderer->enqueue(task, event);
}

void
Renderer_Canvas::free_pixbuf_data_callback(const guint8 *x)
	{ delete[] x; }

void
Renderer_Canvas::on_tile_finished_callback(bool success, Renderer_Canvas *obj, Tile::Handle tile)
{
	// Handle will protect 'tile' from deletion before this call
	// This callback will called only once for each tile
	// And will called before deletion of 'obj', by calling cancel_render() from destructor
	obj->on_tile_finished(success, tile);
}

void
Renderer_Canvas::post_tile_finished_callback(etl::handle<Renderer_Canvas> obj) {
	// this function should be called in main thread
	// Handle will protect 'obj' from deletion before this call
	if (obj->get_work_area()) {
		obj->get_work_area()->queue_draw();
		if (obj->render_queued) obj->enqueue_render();
	}
}

void
Renderer_Canvas::inc_refresh_id()
{
	Glib::Threads::Mutex::Lock lock(mutex);
	++refresh_id;
}

void
Renderer_Canvas::cancel_render(long long keep_refresh_id)
{
	rendering::Task::List list;
	while(true) {
		{
			Glib::Threads::Mutex::Lock lock(mutex);
			render_queued = false;
			for(TileMap::const_iterator i = tiles.begin(); i != tiles.end(); ++i)
				for(TileSet::const_iterator j = i->second.begin(); j != i->second.end(); ++j)
					if (*j && (*j)->event && !(*j)->event->is_finished() && (*j)->refresh_id < keep_refresh_id)
						list.push_back((*j)->event);
		}
		if (list.empty()) return;
		rendering::Renderer::cancel(list);
		list.clear();
		Glib::usleep(1);
	}
}

void
Renderer_Canvas::on_tile_finished(bool success, const Tile::Handle &tile)
{
	// this method must be called from on_tile_finished_callback()
	// only this non-static method may be called in other threads

	// 'tiles', 'onion_frames' and 'refresh_id' are controlled by mutex

	Glib::Threads::Mutex::Lock lock(mutex);

	// release event
	tile->event.reset();

	// convert surface
	if (success && tile->surface) {
		rendering::SurfaceResource::LockReadBase surface_lock(tile->surface);
		if (surface_lock) {
			const rendering::Surface &surface = *surface_lock.get_surface();
			int w = surface.get_width();
			int h = surface.get_height();
			int count = w*h;

			if ( w > 0 && w == tile->rect.get_width()
			  && h > 0 && h == tile->rect.get_height() )
			{
				const Color *pixels = surface.get_pixels_pointer();
				std::vector<Color> pixels_copy;
				if (!pixels) {
					pixels_copy.resize(count);
					if (surface.get_pixels(&pixels_copy.front()))
						pixels = &pixels_copy.front();
				}

				if (pixels) {
					// convert to RGBA32
					PixelFormat pf(PF_RGB | PF_A);
					int pixel_size = synfig::channels(pf);
					unsigned char *buffer = new unsigned char[count*pixel_size];
					unsigned char *dest(buffer);
					const Color *src = pixels;
					const Color *end = src + surface.get_pixels_count();
					while(src < end)
						dest = Color2PixelFormat(
								(src++)->clamped(),
								pf,
								dest,
								App::gamma );

					// create Gdk::PixBuf
					tile->pixbuf = Gdk::Pixbuf::create_from_data(
							buffer, Gdk::COLORSPACE_RGB, pf & PF_A,
							8, w, h, count*pixel_size,
							sigc::ptr_fun(&free_pixbuf_data_callback) );
				}
			}
		}
	}

	// release surface
	tile->surface.reset();

	// new tile is ready
	// let's now remove old tiles
	for(TileMap::iterator i = tiles.begin(); i != tiles.end(); ++i) {
		bool is_frame_visible = false;
		for(FrameList::const_iterator j = onion_frames.begin(); j != onion_frames.end(); ++j)
			if (j->time == i->first) { is_frame_visible = true; break; }

		// remove bad tiles
		for(TileSet::iterator j = i->second.begin(); j != i->second.end(); )
			if ( !*j                                                    // remove nulls (possible bug)
			  || (!(*j)->event && !(*j)->surface)                       // remove tiles which was fail to render
			  || (!is_frame_visible && (*j)->refresh_id < refresh_id) ) // remove outdated tiles at invisible frame
					i->second.erase(j++); else ++j;

		// remove overlapped and partially overlapped tiles
		// note: tiles are sorted by refresh_id - older first
		for(TileSet::iterator j = i->second.begin(); j != i->second.end(); ) {
			bool overlapped = false;
			TileSet::iterator k = j;
			while(++k != i->second.end())
				if ((*k)->pixbuf && etl::intersect((*j)->rect, (*k)->rect))
					{ overlapped = true; break; }
			if (overlapped)
				i->second.erase(j++); else ++j;
		}
	}

	// don't create handle if ref-count is zero
	// it means that object was nether had a handles and will removed with handle
	// or object is already in destruction phase
	if (shared_object::count())
		Glib::signal_timeout().connect_once(
			sigc::bind(sigc::ptr_fun(&post_tile_finished_callback), etl::handle<Renderer_Canvas>(this)), 0);
}

void
Renderer_Canvas::enqueue_render(bool force)
{
	ColorReal base_onion_alpha = 0.75;
	const int tile_grid_step = 64;

	assert(get_work_area());
	rendering::Task::List tasks_to_cancel;
	long long current_refresh_id;

	{
		Glib::Threads::Mutex::Lock lock(mutex);
		current_refresh_id = refresh_id;

		if (!force) {
			render_queued = true;
			// if any outdated tile still in process, then return
			for(TileMap::const_iterator i = tiles.begin(); i != tiles.end(); ++i)
				for(TileSet::const_iterator j = i->second.begin(); j != i->second.end(); ++j)
					if (*j && (*j)->refresh_id < current_refresh_id && (*j)->event && !(*j)->event->is_finished())
						return;
		}
		render_queued = false;

		String renderer_name = get_work_area()->get_renderer();
		rendering::Renderer::Handle renderer = rendering::Renderer::get_renderer(renderer_name);
		if (!renderer) return;

		Canvas::Handle canvas         = get_work_area()->get_canvas();
		RendDesc       rend_desc      = get_work_area()->get_rend_desc();
		Time           base_time      = get_work_area()->get_time();
		VectorInt      window_offset  = get_work_area()->get_windows_offset();
		RectInt        window_rect    = get_work_area()->get_window_rect();
		RectInt        tile_rect      = window_rect + window_offset;
		float          fps            = rend_desc.get_frame_rate();

		ContextParams context_params(rend_desc.get_render_excluded_contexts());

		// apply onion skin
		onion_frames.clear();
		if (get_work_area()->get_onion_skin() && approximate_not_equal_lp(fps, 0.f)) {
			Time frame_duration(1.0/(double)fps);
			int past = get_work_area()->get_onion_skins()[0];
			int future = get_work_area()->get_onion_skins()[1];
			for(int i = 0; i < past; ++i) {
				Time time = base_time - frame_duration*(past - i);
				ColorReal alpha = base_onion_alpha*(double)(i + 1)/(double)(past + 1);
				if (time >= rend_desc.get_time_start() && time <= rend_desc.get_time_end())
					onion_frames.push_back(FrameDesc(base_time, alpha));
			}
			for(int i = 0; i < future; ++i) {
				Time time = base_time + frame_duration*(future - i);
				ColorReal alpha = base_onion_alpha*(double)(i + 1)/(double)(future + 1);
				if (time >= rend_desc.get_time_start() && time <= rend_desc.get_time_end())
					onion_frames.push_back(FrameDesc(base_time, alpha));
			}
			onion_frames.push_back(FrameDesc(base_time, base_onion_alpha));
		} else {
			onion_frames.push_back(FrameDesc(base_time, 1.f));
		}

		// generate rendering tasks
		if (canvas && tile_rect.is_valid()) {
			Time orig_time = canvas->get_time();

			CanvasBase sub_queue;
			std::vector<synfig::RectInt> rects;
			for(FrameList::const_iterator i = onion_frames.begin(); i != onion_frames.end(); ++i) {
				Time frame_time = i->time;
				TileSet &frame_tiles = tiles[frame_time];

				// find not actual regions
				rects.clear();
				rects.push_back(tile_rect);
				for(TileSet::const_iterator j = frame_tiles.begin(); j != frame_tiles.end(); ++j)
					if (*j && (*j)->refresh_id == current_refresh_id)
						etl::rects_subtract(rects, (*j)->rect);
				rects_merge(rects);

				if (!rects.empty()) {
					// build rendering task
					canvas->set_time(frame_time);
					canvas->set_outline_grow(rend_desc.get_outline_grow());
					Context context = canvas->get_context_sorted(context_params, sub_queue);
					rendering::Task::Handle task = context.build_rendering_task();
					sub_queue.clear();

					// add transformation task to flip result if needed
					Vector p0 = rend_desc.get_tl();
					Vector p1 = rend_desc.get_br();
					if (p0[0] > p1[0] || p0[1] > p1[1]) {
						Matrix m;
						if (p0[0] > p1[0]) { m.m00 = -1.0; m.m20 = p0[0] + p1[0]; std::swap(p0[0], p1[0]); }
						if (p0[1] > p1[1]) { m.m11 = -1.0; m.m21 = p0[1] + p1[1]; std::swap(p0[1], p1[1]); }
						rendering::TaskTransformationAffine::Handle t = new rendering::TaskTransformationAffine();
						t->transformation->matrix = m;
						t->sub_task() = task;
						task = t;
					}

					rendering::Task::List list;
					list.push_back(task);

					for(std::vector<synfig::RectInt>::iterator j = rects.begin(); j != rects.end(); ++j) {
						// snap rect corners to tile grid
						RectInt &rect = *j;
						rect.minx = int_floor(rect.minx, tile_grid_step);
						rect.miny = int_floor(rect.miny, tile_grid_step);
						rect.maxx = int_ceil (rect.maxx, tile_grid_step);
						rect.maxy = int_ceil (rect.maxy, tile_grid_step);

						RendDesc tile_desc=rend_desc;
						tile_desc.set_subwindow(rect.minx, rect.miny, rect.maxx - rect.minx, rect.maxy - rect.miny);

						rendering::Task::Handle tile_task = task->clone_recursive();
						tile_task->target_surface = new rendering::SurfaceResource();
						tile_task->target_rect = RectInt( 0, 0, tile_desc.get_w(), tile_desc.get_h() );
						tile_task->source_rect = Rect(p0, p1);

						Tile::Handle tile = new Tile(current_refresh_id, frame_time, *j);
						tile->surface = tile_task->target_surface;

						rendering::TaskEvent::Handle event = new rendering::TaskEvent();
						event->signal_finished.connect( sigc::bind(
							sigc::ptr_fun(&on_tile_finished_callback), this, tile ));

						frame_tiles.insert(tile);

						// Renderer::enqueue contains the expensive 'optimization' stage, so call it async
						ThreadPool::instance.enqueue( sigc::bind(
							sigc::ptr_fun(&enqueue_rendering_task_callback),
							renderer, task, event ));
					}
				}
			}
			canvas->set_time(orig_time);
		}
	}

	// cancel all previous tasks
	cancel_render(current_refresh_id);
}

void
Renderer_Canvas::wait_render()
{
	rendering::TaskEvent::List list;
	while(true) {
		{
			Glib::Threads::Mutex::Lock lock(mutex);
			for(TileMap::const_iterator i = tiles.begin(); i != tiles.end(); ++i)
				for(TileSet::const_iterator j = i->second.begin(); j != i->second.end(); ++j)
					if (*j && (*j)->event && !(*j)->event->is_finished())
						list.push_back((*j)->event);
		}
		if (list.empty()) return;
		for(rendering::TaskEvent::List::iterator i = list.begin(); i != list.end(); ++i)
			(*i)->wait();
		list.clear();
		Glib::usleep(1);
	}
}

void
Renderer_Canvas::render_vfunc(
	const Glib::RefPtr<Gdk::Window>& drawable,
	const Gdk::Rectangle& expose_area )
{
	VectorInt window_offset = get_work_area()->get_windows_offset();
	RectInt   window_rect   = get_work_area()->get_window_rect();
	RectInt   expose_rect   = RectInt( expose_area.get_x(),
			                           expose_area.get_y(),
									   expose_area.get_x() + expose_area.get_width(),
									   expose_area.get_y() + expose_area.get_height() );
	expose_rect &= window_rect;
	if (!expose_rect.is_valid()) return;

	// enqueue rendering if not all of visible tiles are exists and actual
	enqueue_render();

	Glib::Threads::Mutex::Lock lock(mutex);

	ColorReal summary_alpha = 0.f;
	if (onion_frames.empty()) return;
	for(FrameList::const_iterator i = onion_frames.begin(); i != onion_frames.end(); ++i)
		summary_alpha = summary_alpha*(1.f - i->alpha) + i->alpha;
	if (approximate_less_or_equal_lp(summary_alpha, 0.f)) return;

	Cairo::RefPtr<Cairo::Context> context = drawable->create_cairo_context();

	// context for tiles
	Cairo::RefPtr<Cairo::ImageSurface> onion_surface;
	Cairo::RefPtr<Cairo::Context> onion_context = context;
	if (approximate_less(summary_alpha, 1.f)) {
		// create surface to merge onion skin
		onion_surface = Cairo::ImageSurface::create(
				Cairo::FORMAT_ARGB32, expose_rect.get_width(), expose_rect.get_height() );
		onion_context = Cairo::Context::create(onion_surface);
		onion_context->translate(-(double)expose_rect.minx, -(double)expose_rect.miny);
	}

	// draw tiles
	onion_context->save();
	onion_context->translate((double)window_offset[0], (double)window_offset[1]);
	for(FrameList::const_iterator i = onion_frames.begin(); i != onion_frames.end(); ++i) {
		TileMap::const_iterator ii = tiles.find(i->time);
		if (ii == tiles.end()) continue;
		for(TileSet::const_iterator j = ii->second.begin(); j != ii->second.end(); ++j) {
			if (*j && (*j)->pixbuf) {
				Gdk::Cairo::set_source_pixbuf(
					onion_context,
					(*j)->pixbuf,
					(*j)->rect.minx,
					(*j)->rect.miny );
				onion_context->paint_with_alpha(i->alpha);
			}
		}
	}
	onion_context->restore();

	// finish with onion skin
	if (onion_surface) {
		assert(onion_context != context);
		onion_context.clear(); // release onion context
		onion_surface->flush();

		// normalize alpha
		ColorReal k = 255.f/summary_alpha;
		unsigned char *data = onion_surface->get_data();
		int stride = onion_surface->get_stride();
		int image_size = stride*onion_surface->get_height();
		for(unsigned char *row = data, *image_end = data + image_size; row < image_end; row += stride)
			for(unsigned char *pixel = row, *row_end = row + stride; pixel < row_end; pixel += 4)
				*pixel = (unsigned char)round(max(0.f, min(255.f, ((ColorReal)(*pixel)*k))));

		// put merged onion to context
		context->save();
		context->set_source(onion_surface, (double)expose_rect.minx, (double)expose_rect.miny);
		context->paint();
		context->restore();
	}

	// draw the border around the rendered region
	context->save();
	context->set_line_cap(Cairo::LINE_CAP_BUTT);
	context->set_line_join(Cairo::LINE_JOIN_MITER);
	context->set_antialias(Cairo::ANTIALIAS_NONE);
	context->set_line_width(1.0);
	context->set_source_rgb(0,0,0);
	context->rectangle(
		(double)window_offset[0],
		(double)window_offset[1],
		(double)get_w(),
		(double)get_h() );
	context->stroke();
	context->restore();
}