PsychoBlend now updates render in realtime within Blender.

Also, cancelling renders is much more responsive now, especially
during long exports.
This commit is contained in:
Nathan Vegdahl 2017-06-03 00:00:19 -07:00
parent 3beffab507
commit f84d093f66
7 changed files with 226 additions and 92 deletions

34
Cargo.lock generated
View File

@ -17,11 +17,24 @@ dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "base64"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "bitflags"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "byteorder"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "clap"
version = "2.24.2"
@ -51,7 +64,7 @@ dependencies = [
[[package]]
name = "gcc"
version = "0.3.49"
version = "0.3.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@ -100,7 +113,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "num_cpus"
version = "1.5.0"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
@ -121,7 +134,7 @@ name = "openexr-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"gcc 0.3.49 (registry+https://github.com/rust-lang/crates.io-index)",
"gcc 0.3.50 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -140,6 +153,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
name = "psychopath"
version = "0.1.0"
dependencies = [
"base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.24.2 (registry+https://github.com/rust-lang/crates.io-index)",
"crossbeam 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
"float4 0.1.0",
@ -149,7 +163,7 @@ dependencies = [
"math3d 0.1.0",
"mem_arena 0.1.0",
"nom 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"openexr 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"png_encode_mini 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)",
@ -161,7 +175,7 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.1.17"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@ -205,7 +219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -237,21 +251,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[metadata]
"checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6"
"checksum atty 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d912da0db7fa85514874458ca3651fe2cddace8d0b0505571dbdcd41ab490159"
"checksum base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "30e93c03064e7590d0466209155251b90c22e37fab1daf2771582598b5827557"
"checksum bitflags 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1370e9fc2a6ae53aea8b7a5110edbd08836ed87c88736dfabccade1c2b44bff4"
"checksum byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c40977b0ee6b9885c9013cd41d9feffdd22deb3bb4dc3a71d901cc7a77de18c8"
"checksum clap 2.24.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6b8f69e518f967224e628896b54e41ff6acfb4dcfefc5076325c36525dac900f"
"checksum crossbeam 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "0c5ea215664ca264da8a9d9c3be80d2eaf30923c259d03e870388eb927508f97"
"checksum gcc 0.3.49 (registry+https://github.com/rust-lang/crates.io-index)" = "9be730064c122681712957ba1a9abaf082150be8aaf94526a805d900015b65b9"
"checksum gcc 0.3.50 (registry+https://github.com/rust-lang/crates.io-index)" = "5f837c392f2ea61cb1576eac188653df828c861b7137d74ea4a5caa89621f9e6"
"checksum half 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "63d68db75012a85555434ee079e7e6337931f87a087ab2988becbadf64673a7f"
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
"checksum lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3b37545ab726dd833ec6420aaba8231c5b320814b9029ad585555d2a03e94fbf"
"checksum libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)" = "e7eb6b826bfc1fdea7935d46556250d1799b7fe2d9f7951071f4291710665e3e"
"checksum nom 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce"
"checksum num_cpus 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f6e850c7f35c3de263e6094e819f6b4b9c09190ff4438fc6dec1aef1568547bc"
"checksum num_cpus 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6e416ba127a4bb3ff398cb19546a8d0414f73352efe2857f4060d36f5fe5983a"
"checksum openexr 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "849a5af32d6b0716cf44a8b18c1b611b8448aec44a9b1c59b6199bf9d22aba94"
"checksum openexr-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d597730c7049d9098ce64bb97c6eb4e6f96fa55d5ced7eba74a01c224fe0bcc4"
"checksum pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3a8b4c6b8165cd1a1cd4b9b120978131389f64bdaf456435caa41e630edba903"
"checksum png_encode_mini 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "116ae962ea1a679f99915c2c53dc470b45aaf6b42e607580d16fce8c6248f666"
"checksum redox_syscall 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "29dbdfd4b9df8ab31dec47c6087b7b13cbf4a776f335e4de8efba8288dda075b"
"checksum redox_syscall 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "3041aeb6000db123d2c9c751433f526e1f404b23213bd733167ab770c3989b4d"
"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
"checksum scoped_threadpool 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "3ef399c8893e8cb7aa9696e895427fab3a6bf265977bb96e126f24ddd2cda85a"
"checksum simd 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a94d14a2ae1f1f110937de5fb69e494372560181c7e1739a097fcc2cee37ba0"

View File

@ -21,6 +21,7 @@ debug = true
[dependencies]
# Crates.io dependencies
base64 = "0.5"
clap = "2.23"
crossbeam = "0.2"
half = "1.0"

View File

@ -3,6 +3,12 @@ import bpy
from math import degrees, pi, log
from mathutils import Vector, Matrix
class ExportCancelled(Exception):
""" Indicates that the render was cancelled in the middle of exporting
the scene file.
"""
pass
def mat2str(m):
""" Converts a matrix into a single-line string of values.
@ -87,7 +93,8 @@ class IndentedWriter:
class PsychoExporter:
def __init__(self, scene):
def __init__(self, render_engine, scene):
self.render_engine = render_engine
self.scene = scene
self.mesh_names = {}
@ -112,9 +119,22 @@ class PsychoExporter:
else:
self.scene.frame_set(frame-1, 1.0+fraction)
def export_psy(self, export_path, render_image_path):
try:
f = open(export_path, 'w')
self._export_psy(f, export_path, render_image_path)
except ExportCancelled:
# Cleanup
f.close()
self.scene.frame_set(self.fr)
return False
else:
# Cleanup
f.close()
self.scene.frame_set(self.fr)
return True
def _export_psy(self, f, export_path, render_image_path):
self.w = IndentedWriter(f)
# Info
@ -168,6 +188,10 @@ class PsychoExporter:
matz = Matrix()
matz[2][2] = -1
for i in range(self.time_samples):
# Check if render is cancelled
if self.render_engine.test_break():
raise ExportCancelled()
if res_x >= res_y:
self.w.write("Fov [%f]\n" % degrees(cam.data.angle))
else:
@ -222,9 +246,6 @@ class PsychoExporter:
self.w.unindent()
self.w.write("}\n")
# Cleanup
f.close()
self.scene.frame_set(self.fr)
def export_materials(self, materials):
@ -243,6 +264,10 @@ class PsychoExporter:
def export_objects(self, objects, visible_layers, group_prefix="", translation_offset=(0,0,0)):
for ob in objects:
# Check if render is cancelled
if self.render_engine.test_break():
raise ExportCancelled()
# Check if the object is visible for rendering
vis_layer = False
for i in range(len(ob.layers)):
@ -278,6 +303,9 @@ class PsychoExporter:
if needs_xform_mb(ob):
for i in range(self.time_samples):
# Check if render is cancelled
if self.render_engine.test_break():
raise ExportCancelled()
self.set_frame(self.fr, self.shutter_start + (self.shutter_diff*i))
mat = ob.matrix_world.copy()
mat[0][3] += translation_offset[0]
@ -316,6 +344,9 @@ class PsychoExporter:
# Collect time samples
time_meshes = []
for i in range(self.time_samples):
# Check if render is cancelled
if self.render_engine.test_break():
raise ExportCancelled()
self.set_frame(self.fr, self.shutter_start + (self.shutter_diff*i))
if export_mesh and (deform_mb or i == 0):
time_meshes += [ob.to_mesh(self.scene, True, 'RENDER')]
@ -366,6 +397,9 @@ class PsychoExporter:
# Collect time samples
time_surfaces = []
for i in range(self.time_samples):
# Check if render is cancelled
if self.render_engine.test_break():
raise ExportCancelled()
self.set_frame(self.fr, self.shutter_start + (self.shutter_diff*i))
time_surfaces += [ob.data.copy()]
@ -393,6 +427,9 @@ class PsychoExporter:
time_col = []
time_rad = []
for i in range(self.time_samples):
# Check if render is cancelled
if self.render_engine.test_break():
raise ExportCancelled()
self.set_frame(self.fr, self.shutter_start + (self.shutter_diff*i))
time_col += [ob.data.color * ob.data.energy]
time_rad += [ob.data.shadow_soft_size]
@ -417,6 +454,9 @@ class PsychoExporter:
time_col = []
time_dim = []
for i in range(self.time_samples):
# Check if render is cancelled
if self.render_engine.test_break():
raise ExportCancelled()
self.set_frame(self.fr, self.shutter_start + (self.shutter_diff*i))
time_col += [ob.data.color * ob.data.energy]
if ob.data.shape == 'RECTANGLE':
@ -446,6 +486,9 @@ class PsychoExporter:
time_col = []
time_rad = []
for i in range(self.time_samples):
# Check if render is cancelled
if self.render_engine.test_break():
raise ExportCancelled()
self.set_frame(self.fr, self.shutter_start + (self.shutter_diff*i))
time_dir += [tuple(ob.matrix_world.to_3x3() * Vector((0, 0, -1)))]
time_col += [ob.data.color * ob.data.energy]

View File

@ -3,6 +3,8 @@ import time
import os
import subprocess
import tempfile
import base64
import struct
from . import psy_export
def get_temp_filename(suffix=""):
@ -39,8 +41,8 @@ class PsychopathRender(bpy.types.RenderEngine):
return ""
def _export(self, scene, export_path, render_image_path):
exporter = psy_export.PsychoExporter(scene)
exporter.export_psy(export_path, render_image_path)
exporter = psy_export.PsychoExporter(self, scene)
return exporter.export_psy(export_path, render_image_path)
def _render(self, scene, psy_filepath):
psy_binary = PsychopathRender._locate_binary()
@ -49,7 +51,7 @@ class PsychopathRender(bpy.types.RenderEngine):
return False
# TODO: figure out command line options
args = ["--spb", str(scene.psychopath.max_samples_per_bucket), "-i", psy_filepath]
args = ["--spb", str(scene.psychopath.max_samples_per_bucket), "--blender_output", "-i", psy_filepath]
# Start Rendering!
try:
@ -65,27 +67,24 @@ class PsychopathRender(bpy.types.RenderEngine):
return True
def _draw_bucket(self, bucket_info, pixels_encoded):
x = bucket_info[0]
y = self.size_y - bucket_info[3]
width = bucket_info[2] - bucket_info[0]
height = bucket_info[3] - bucket_info[1]
def _cleanup(self):
# for f in (self._temp_file_in, self._temp_file_ini, self._temp_file_out):
# for i in range(5):
# try:
# os.unlink(f)
# break
# except OSError:
# # Wait a bit before retrying file might be still in use by Blender,
# # and Windows does not know how to delete a file in use!
# time.sleep(self.DELAY)
# for i in unpacked_images:
# for c in range(5):
# try:
# os.unlink(i)
# break
# except OSError:
# # Wait a bit before retrying file might be still in use by Blender,
# # and Windows does not know how to delete a file in use!
# time.sleep(self.DELAY)
pass
# Decode pixel data
pixels = [p for p in struct.iter_unpack("ffff", base64.b64decode(pixels_encoded))]
pixels_flipped = []
for i in range(height):
n = height - i - 1
pixels_flipped += pixels[n*width:(n+1)*width]
# Write pixel data to render image
result = self.begin_result(x, y, width, height)
lay = result.layers[0].passes["Combined"]
lay.rect = pixels_flipped
self.end_result(result)
def render(self, scene):
# has to be called to update the frame on exporting animations
@ -103,7 +102,10 @@ class PsychopathRender(bpy.types.RenderEngine):
# start export
self.update_stats("", "Psychopath: Exporting data from Blender")
self._export(scene, export_path, render_image_path)
if not self._export(scene, export_path, render_image_path):
# Render cancelled in the middle of exporting,
# so just return.
return
# Start rendering
self.update_stats("", "Psychopath: Rendering from exported file")
@ -113,54 +115,78 @@ class PsychopathRender(bpy.types.RenderEngine):
r = scene.render
# compute resolution
x = int(r.resolution_x * r.resolution_percentage)
y = int(r.resolution_y * r.resolution_percentage)
self.size_x = int(r.resolution_x * r.resolution_percentage / 100)
self.size_y = int(r.resolution_y * r.resolution_percentage / 100)
result = self.begin_result(0, 0, x, y)
lay = result.layers[0]
# If we can, make the render process's stdout non-blocking. The
# benefit of this is that canceling the render won't block waiting
# for the next piece of input.
try:
import fcntl
fd = self._process.stdout.fileno()
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
except:
print("NOTE: Can't make Psychopath's stdout non-blocking, so canceling renders may take a moment to respond.")
# TODO: Update viewport with render result while rendering
# Process output from rendering process
reached_first_bucket = False
output = b""
while self._process.poll() == None:
# Wait for self.DELAY seconds, but check for render cancels
# and progress updates while waiting.
t = 0.0
while t < self.DELAY:
# Wait for render process output while checking for render
# cancellation
while True:
# Check for render cancel
if self.test_break():
self._process.terminate()
break
# Get render output from stdin
tmp = self._process.stdout.read1(2**16)
if len(tmp) == 0:
time.sleep(0.01)
continue
else:
break
output += tmp
outputs = output.split(b'DIV\n')
# Skip render process output until we hit the first bucket.
# (The stuff before it is just informational printouts.)
if not reached_first_bucket:
if len(outputs) > 1:
reached_first_bucket = True
outputs = outputs[1:]
else:
continue
# Clear output buffer, since it's all in 'outputs' now.
output = b""
# Process buckets
for bucket in outputs:
if len(bucket) == 0:
continue
if bucket[-11:] == b'BUCKET_END\n':
# Parse bucket text
contents = bucket.split(b'\n')
percentage = contents[0]
bucket_info = [int(i) for i in contents[1].split(b' ')]
pixels = contents[2]
# Draw the bucket
self._draw_bucket(bucket_info, pixels)
# Update render progress bar
output += self._process.stdout.read1(2**16)
outputs = output.rsplit(b'\r')
progress = 0.0
if len(outputs) > 0 and outputs[-1][-1] == b"%"[0]:
try:
progress = float(outputs[-1][:-1])
progress = float(percentage[:-1])
except ValueError:
pass
finally:
self.update_progress(progress/100)
time.sleep(0.05)
t += 0.05
# # Update viewport image with latest render output
# if os.path.exists(render_image_path):
# # This assumes the file has been fully written We wait a bit, just in case!
# try:
# lay.load_from_file(render_image_path)
# self.update_result(result)
# except RuntimeError:
# pass
# Load final image
lay.load_from_file(render_image_path)
self.end_result(result)
# Delete temporary image file
os.remove(render_image_path)
else:
output += bucket
def register():
bpy.utils.register_class(PsychopathRender)

View File

@ -220,6 +220,33 @@ impl<'a> Bucket<'a> {
data[img.res.0 * y as usize + x as usize] = value;
}
/// Returns the bucket's contents encoded in base64.
///
/// `color_convert` lets you do a colorspace conversion before base64
/// encoding if desired.
///
/// The data is laid out as four-floats-per-pixel in scanline order before
/// encoding to base64. The fourth channel is alpha, and is set to 1.0 for
/// all pixels.
pub fn rgba_base64<F>(&mut self, color_convert: F) -> String
where F: Fn((f32, f32, f32)) -> (f32, f32, f32)
{
use base64;
use std::slice;
let mut data = Vec::with_capacity((4 * (self.max.0 - self.min.0) * (self.max.1 - self.min.1)) as usize);
for y in self.min.1..self.max.1 {
for x in self.min.0..self.max.0 {
let color = color_convert(self.get(x, y).to_tuple());
data.push(color.0);
data.push(color.1);
data.push(color.2);
data.push(1.0);
}
}
let data_u8 = unsafe { slice::from_raw_parts(&data[0] as *const f32 as *const u8, data.len() * 4) };
base64::encode(data_u8)
}
}
impl<'a> Drop for Bucket<'a> {

View File

@ -4,6 +4,7 @@ extern crate math3d;
extern crate mem_arena;
extern crate spectra_xyz;
extern crate base64;
extern crate clap;
extern crate crossbeam;
extern crate half;
@ -141,6 +142,12 @@ fn main() {
.long("dev")
.help("Show useful dev/debug info.")
)
.arg(
Arg::with_name("blender_output")
.long("blender_output")
.help("Used by PsychoBlend to pass render output through standard in/out.")
.hidden(true)
)
.get_matches();
// Print some misc useful dev info.
@ -207,7 +214,11 @@ fn main() {
println!("\tBuilt scene in {:.3}s", t.tick());
println!("Rendering scene with {} threads...", thread_count);
let (mut image, rstats) = r.render(max_samples_per_bucket, thread_count);
let (mut image, rstats) = r.render(
max_samples_per_bucket,
thread_count,
args.is_present("blender_output"),
);
// Print render stats
{
let rtime = t.tick();
@ -235,6 +246,7 @@ fn main() {
);
}
if !args.is_present("blender_output") {
println!("Writing image to disk...");
if r.output_file.ends_with(".png") {
let _ = image.write_png(Path::new(&r.output_file));
@ -244,6 +256,7 @@ fn main() {
panic!("Unknown output file extension.");
}
println!("\tWrote image in {:.3}s", t.tick());
}
// Print memory stats if stats are wanted.
if args.is_present("stats") {

View File

@ -68,7 +68,7 @@ impl RenderStats {
}
impl<'a> Renderer<'a> {
pub fn render(&self, max_samples_per_bucket: u32, thread_count: u32) -> (Image, RenderStats) {
pub fn render(&self, max_samples_per_bucket: u32, thread_count: u32, do_blender_output: bool) -> (Image, RenderStats) {
let mut tpool = Pool::new(thread_count);
let image = Image::new(self.resolution.0, self.resolution.1);
@ -94,7 +94,7 @@ impl<'a> Renderer<'a> {
let img = &image;
let cstats = &collective_stats;
let pixrenref = &pixels_rendered;
scope.execute(move || self.render_job(jq, ajq, img, cstats, pixrenref));
scope.execute(move || self.render_job(jq, ajq, img, cstats, pixrenref, do_blender_output));
}
// Print initial 0.00% progress
@ -164,7 +164,7 @@ impl<'a> Renderer<'a> {
}
/// Waits for buckets in the job queue to render and renders them when available.
fn render_job(&self, job_queue: &MsQueue<BucketJob>, all_jobs_queued: &RwLock<bool>, image: &Image, collected_stats: &RwLock<RenderStats>, pixels_rendered: &Mutex<Cell<usize>>) {
fn render_job(&self, job_queue: &MsQueue<BucketJob>, all_jobs_queued: &RwLock<bool>, image: &Image, collected_stats: &RwLock<RenderStats>, pixels_rendered: &Mutex<Cell<usize>>, do_blender_output: bool) {
let mut stats = RenderStats::new();
let mut timer = Timer::new();
let mut total_timer = Timer::new();
@ -251,8 +251,8 @@ impl<'a> Renderer<'a> {
stats.ray_generation_time += timer.tick() as f64;
}
// Calculate color based on ray hits and save to image
{
// Calculate color based on ray hits and save to image
let min = (bucket.x, bucket.y);
let max = (bucket.x + bucket.w, bucket.y + bucket.h);
let mut img_bucket = image.get_bucket(min, max);
@ -263,10 +263,8 @@ impl<'a> Renderer<'a> {
img_bucket.set(path.pixel_co.0, path.pixel_co.1, col);
}
stats.sample_writing_time += timer.tick() as f64;
}
// Print render progress
{
// Print render progress, and image data if doing blender output
let guard = pixels_rendered.lock().unwrap();
let mut pr = (*guard).get();
let percentage_old = pr as f64 / total_pixels as f64 * 100.0;
@ -278,11 +276,21 @@ impl<'a> Renderer<'a> {
let old_string = format!("{:.2}%", percentage_old);
let new_string = format!("{:.2}%", percentage_new);
if do_blender_output {
use color::xyz_to_rec709e;
println!("DIV");
println!("{}", new_string);
println!("{} {} {} {}", min.0, min.1, max.0, max.1);
println!("{}", img_bucket.rgba_base64(xyz_to_rec709e));
println!("BUCKET_END");
println!("DIV");
} else {
if new_string != old_string {
print!("\r{}", new_string);
let _ = io::stdout().flush();
}
}
let _ = io::stdout().flush();
}
}
stats.total_time += total_timer.tick() as f64;