Cycles: Resumable render implementation for Cycles
authorSergey Sharybin <sergey.vfx@gmail.com>
Wed, 30 Mar 2016 13:55:12 +0000 (15:55 +0200)
committerSergey Sharybin <sergey.vfx@gmail.com>
Wed, 30 Mar 2016 14:02:27 +0000 (16:02 +0200)
This feature is also known by the name Samples Offset, which allows
artists to render animation with given amount of samples N, but then
render more samples, starting from N and ending with M (where M > N)
and merge renders together as if they rendered exactly M samples.

Surely such effect could be achieved by changing Seed variable, but
that has possible issues with correlation artifacts and requiring to
manually deal with per render layer samples and such.

While we can't support all possible renderfarm-related features in
Cycles it's nice to support really commonly used stuff.

Here's a command how to run Blender with the new feature enabled:

  blender -- --cycles-resumable-num-chunks 24 --cycles-resumable-current-chunk 2

This command will divide samples range in 24 parts and render
range #2 (chunk number is 1-based).

This feature might be changed a bit after we'll do some tests here
in the studio with it.

intern/cycles/blender/addon/engine.py
intern/cycles/blender/blender_python.cpp
intern/cycles/blender/blender_session.cpp
intern/cycles/blender/blender_session.h
intern/cycles/render/tile.cpp
intern/cycles/render/tile.h

index 96dc3a59ef2df73319241f85a5f5ad8635e3934b..d4b7535b9eebf586a38d183605f498d101809ab1 100644 (file)
@@ -50,10 +50,39 @@ def _workaround_buggy_drivers():
             _cycles.opencl_disable()
 
 
+def _parse_command_line():
+    import sys
+
+    argv = sys.argv
+    if "--" not in argv:
+        return
+
+    argv = argv[argv.index("--") + 1:]
+
+    num_resumable_chunks = None
+    current_resumable_chunk = None
+
+    # TODO(sergey): Add some nice error ptins if argument is not used properly.
+    idx = 0
+    while idx < len(argv) - 1:
+        arg = argv[idx]
+        if arg == '--cycles-resumable-num-chunks':
+            num_resumable_chunks = int(argv[idx + 1])
+        elif arg == '--cycles-resumable-current-chunk':
+            current_resumable_chunk = int(argv[idx + 1])
+        idx += 1
+
+    if num_resumable_chunks is not None and current_resumable_chunk is not None:
+        import _cycles
+        _cycles.set_resumable_chunks(num_resumable_chunks,
+                                     current_resumable_chunk)
+
+
 def init():
     import bpy
     import _cycles
     import os.path
+    import sys
 
     # Workaround possibly buggy legacy drivers which crashes on the OpenCL
     # device enumeration.
@@ -72,7 +101,7 @@ def init():
     user_path = os.path.dirname(os.path.abspath(bpy.utils.user_resource('CONFIG', '')))
 
     _cycles.init(path, user_path, bpy.app.background)
-
+    _parse_command_line()
 
 def exit():
     import _cycles
index b2294c8b68eda0c4758b9fbd5ec36495854b87c3..989ba80c0015b6dd192ac757abd60dfc136c5f9c 100644 (file)
@@ -638,6 +638,24 @@ static PyObject *debug_flags_reset_func(PyObject * /*self*/, PyObject * /*args*/
        Py_RETURN_NONE;
 }
 
+static PyObject *set_resumable_chunks_func(PyObject * /*self*/, PyObject *args)
+{
+       int num_resumable_chunks, current_resumable_chunk;
+       if(!PyArg_ParseTuple(args, "ii",
+                            &num_resumable_chunks,
+                            &current_resumable_chunk)) {
+               return NULL;
+       }
+
+       VLOG(1) << "Initialized resumable render: "
+               << "num_resumable_chunks=" << num_resumable_chunks << ", "
+               << "current_resumable_chunk=" << current_resumable_chunk;
+       BlenderSession::num_resumable_chunks = num_resumable_chunks;
+       BlenderSession::current_resumable_chunk = current_resumable_chunk;
+
+       Py_RETURN_NONE;
+}
+
 static PyMethodDef methods[] = {
        {"init", init_func, METH_VARARGS, ""},
        {"exit", exit_func, METH_VARARGS, ""},
@@ -657,8 +675,14 @@ static PyMethodDef methods[] = {
 #ifdef WITH_OPENCL
        {"opencl_disable", opencl_disable_func, METH_NOARGS, ""},
 #endif
+
+       /* Debugging routines */
        {"debug_flags_update", debug_flags_update_func, METH_VARARGS, ""},
        {"debug_flags_reset", debug_flags_reset_func, METH_NOARGS, ""},
+
+       /* Resumable render */
+       {"set_resumable_chunks", set_resumable_chunks_func, METH_VARARGS, ""},
+
        {NULL, NULL, 0, NULL},
 };
 
index 977ba3d34cf4a6e4119396c50f8b507890583156..92b5b7277f42fa83e31def49e286f8c243bdef13 100644 (file)
@@ -43,6 +43,8 @@
 CCL_NAMESPACE_BEGIN
 
 bool BlenderSession::headless = false;
+int BlenderSession::num_resumable_chunks = 0;
+int BlenderSession::current_resumable_chunk = 0;
 
 BlenderSession::BlenderSession(BL::RenderEngine& b_engine,
                                BL::UserPreferences& b_userpref,
@@ -161,6 +163,8 @@ void BlenderSession::create_session()
        session->reset(buffer_params, session_params.samples);
 
        b_engine.use_highlight_tiles(session_params.progressive_refine == false);
+
+       update_resumable_tile_manager(session_params.samples);
 }
 
 void BlenderSession::reset_session(BL::BlendData& b_data_, BL::Scene& b_scene_)
@@ -510,14 +514,21 @@ void BlenderSession::render()
                                        &python_thread_state,
                                        b_rlay_name.c_str());
 
-                       /* update number of samples per layer */
+                       /* Update number of samples per layer. */
                        int samples = sync->get_layer_samples();
                        bool bound_samples = sync->get_layer_bound_samples();
+                       int effective_layer_samples;
 
                        if(samples != 0 && (!bound_samples || (samples < session_params.samples)))
-                               session->reset(buffer_params, samples);
+                               effective_layer_samples = samples;
                        else
-                               session->reset(buffer_params, session_params.samples);
+                               effective_layer_samples = session_params.samples;
+
+                       /* Update tile manager if we're doing resumable render. */
+                       update_resumable_tile_manager(effective_layer_samples);
+
+                       /* Update session itself. */
+                       session->reset(buffer_params, effective_layer_samples);
 
                        /* render */
                        session->start();
@@ -712,6 +723,13 @@ void BlenderSession::do_write_update_render_result(BL::RenderResult& b_rr,
 
        vector<float> pixels(params.width*params.height*4);
 
+       /* Adjust absolute sample number to the range. */
+       int sample = rtile.sample;
+       const int range_start_sample = session->tile_manager.range_start_sample;
+       if(range_start_sample != -1) {
+               sample -= range_start_sample;
+       }
+
        if(!do_update_only) {
                /* copy each pass */
                BL::RenderLayer::passes_iterator b_iter;
@@ -724,7 +742,7 @@ void BlenderSession::do_write_update_render_result(BL::RenderResult& b_rr,
                        int components = b_pass.channels();
 
                        /* copy pixels */
-                       if(!buffers->get_pass_rect(pass_type, exposure, rtile.sample, components, &pixels[0]))
+                       if(!buffers->get_pass_rect(pass_type, exposure, sample, components, &pixels[0]))
                                memset(&pixels[0], 0, pixels.size()*sizeof(float));
 
                        b_pass.rect(&pixels[0]);
@@ -733,7 +751,7 @@ void BlenderSession::do_write_update_render_result(BL::RenderResult& b_rr,
        else {
                /* copy combined pass */
                BL::RenderPass b_combined_pass(b_rlay.passes.find_by_type(BL::RenderPass::type_COMBINED, b_rview_name.c_str()));
-               if(buffers->get_pass_rect(PASS_COMBINED, exposure, rtile.sample, 4, &pixels[0]))
+               if(buffers->get_pass_rect(PASS_COMBINED, exposure, sample, 4, &pixels[0]))
                        b_combined_pass.rect(&pixels[0]);
        }
 
@@ -909,12 +927,12 @@ void BlenderSession::get_progress(float& progress, double& total_time, double& r
        int tile, sample, samples_per_tile;
        int tile_total = session->tile_manager.state.num_tiles;
        int samples = session->tile_manager.state.sample + 1;
-       int total_samples = session->tile_manager.num_samples;
+       int total_samples = session->tile_manager.get_num_effective_samples();
 
        session->progress.get_tile(tile, total_time, render_time, tile_time);
 
        sample = session->progress.get_sample();
-       samples_per_tile = session->tile_manager.num_samples;
+       samples_per_tile = session->tile_manager.get_num_effective_samples();
 
        if(background && samples_per_tile && tile_total)
                progress = ((float)sample / (float)(tile_total * samples_per_tile));
@@ -1277,5 +1295,26 @@ bool BlenderSession::builtin_image_float_pixels(const string &builtin_name, void
        return false;
 }
 
-CCL_NAMESPACE_END
+void BlenderSession::update_resumable_tile_manager(int num_samples)
+{
+       const int num_resumable_chunks = BlenderSession::num_resumable_chunks,
+                 current_resumable_chunk = BlenderSession::current_resumable_chunk;
+       if(num_resumable_chunks == 0) {
+               return;
+       }
+
+       int num_samples_per_chunk = (int)ceilf((float)num_samples / num_resumable_chunks);
+       int range_start_sample = num_samples_per_chunk * (current_resumable_chunk - 1);
+       int range_num_samples = num_samples_per_chunk;
+       if(range_start_sample + range_num_samples > num_samples) {
+               range_num_samples = num_samples - range_num_samples;
+       }
 
+       VLOG(1) << "Samples range start is " << range_start_sample << ", "
+               << "number of samples to render is " << range_num_samples;
+
+       session->tile_manager.range_start_sample = range_start_sample;
+       session->tile_manager.range_num_samples = range_num_samples;
+}
+
+CCL_NAMESPACE_END
index 8492d9f2c6bf091a5e93549b8a92e8121d28e874..66a6945cbc1cffc032e732e59fe545dc385d564b 100644 (file)
@@ -95,7 +95,6 @@ public:
        void update_bake_progress();
 
        bool background;
-       static bool headless;
        Session *session;
        Scene *scene;
        BlenderSync *sync;
@@ -120,6 +119,24 @@ public:
 
        void *python_thread_state;
 
+       /* Global state which is common for all render sessions created from Blender.
+        * Usually denotes command line arguments.
+        */
+
+       /* Blender is running from the command line, no windows are shown and some
+        * extra render optimization is possible (possible to free draw-only data and
+        * so on.
+        */
+       static bool headless;
+
+       /* ** Resumable render ** */
+
+       /* Overall number of chunks in which the sample range is to be devided. */
+       static int num_resumable_chunks;
+
+       /* Current resumable chunk index to render. */
+       static int current_resumable_chunk;
+
 protected:
        void do_write_update_render_result(BL::RenderResult& b_rr,
                                           BL::RenderLayer& b_rlay,
@@ -131,6 +148,9 @@ protected:
        void builtin_image_info(const string &builtin_name, void *builtin_data, bool &is_float, int &width, int &height, int &depth, int &channels);
        bool builtin_image_pixels(const string &builtin_name, void *builtin_data, unsigned char *pixels);
        bool builtin_image_float_pixels(const string &builtin_name, void *builtin_data, float *pixels);
+
+       /* Update tile manager to reflect resumable render settings. */
+       void update_resumable_tile_manager(int num_samples);
 };
 
 CCL_NAMESPACE_END
index 3fb60735b65a8fde0cd0cf45281dc84d0f142039..3a6dfea11a737f50a14a05f461bd724337667f2a 100644 (file)
@@ -97,6 +97,9 @@ TileManager::TileManager(bool progressive_, int num_samples_, int2 tile_size_, i
        preserve_tile_device = preserve_tile_device_;
        background = background_;
 
+       range_start_sample = 0;
+       range_num_samples = -1;
+
        BufferParams buffer_params;
        reset(buffer_params, 0);
 }
@@ -124,7 +127,7 @@ void TileManager::reset(BufferParams& params_, int num_samples_)
        num_samples = num_samples_;
 
        state.buffer = BufferParams();
-       state.sample = -1;
+       state.sample = range_start_sample - 1;
        state.num_tiles = 0;
        state.num_rendered_tiles = 0;
        state.num_samples = 0;
@@ -325,7 +328,11 @@ bool TileManager::next_tile(Tile& tile, int device)
 
 bool TileManager::done()
 {
-       return (state.sample+state.num_samples >= num_samples && state.resolution_divider == 1);
+       int end_sample = (range_num_samples == -1)
+                            ? num_samples
+                            : range_start_sample + range_num_samples;
+       return (state.resolution_divider == 1) &&
+              (state.sample+state.num_samples >= end_sample);
 }
 
 bool TileManager::next()
@@ -344,8 +351,10 @@ bool TileManager::next()
 
                if(progressive)
                        state.num_samples = 1;
-               else
+               else if(range_num_samples == -1)
                        state.num_samples = num_samples;
+               else
+                       state.num_samples = range_num_samples;
 
                state.resolution_divider = 1;
                set_tiles();
@@ -354,5 +363,11 @@ bool TileManager::next()
        return true;
 }
 
+int TileManager::get_num_effective_samples()
+{
+       return (range_num_samples == -1) ? num_samples
+                                        : range_num_samples;
+}
+
 CCL_NAMESPACE_END
 
index 700e00c9e0ad3ef0daa4bd0ee0444ac46f4347fe..af1b1ed8b0f25a2acf715458f362ceb3cc8645b3 100644 (file)
@@ -82,6 +82,17 @@ public:
        bool done();
 
        void set_tile_order(TileOrder tile_order_) { tile_order = tile_order_; }
+
+       /* ** Sample range rendering. ** */
+
+       /* Start sample in the range. */
+       int range_start_sample;
+
+       /* Number to samples in the rendering range. */
+       int range_num_samples;
+
+       /* get number of actual samples to render. */
+       int get_num_effective_samples();
 protected:
 
        void set_tiles();