@deepblend/dsh-blender-provider-local 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +1610 -0
- package/package.json +25 -0
- package/python/__pycache__/bootstrap.cpython-314.pyc +0 -0
- package/python/__pycache__/deepblend_capabilities.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_frames.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_frames.cpython-314.pyc +0 -0
- package/python/__pycache__/deepblend_render.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_render.cpython-314.pyc +0 -0
- package/python/__pycache__/deepblend_scene.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_scene.cpython-314.pyc +0 -0
- package/python/__pycache__/deepblend_util.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_util.cpython-314.pyc +0 -0
- package/python/__pycache__/deepblend_validate.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_views.cpython-313.pyc +0 -0
- package/python/bootstrap.py +625 -0
- package/python/deepblend_capabilities.py +633 -0
- package/python/deepblend_frames.py +402 -0
- package/python/deepblend_render.py +299 -0
- package/python/deepblend_scene.py +1537 -0
- package/python/deepblend_util.py +264 -0
- package/python/deepblend_validate.py +358 -0
- package/python/deepblend_views.py +745 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""DeepBlend frame-sequence renderer — one process, an explicit frame list.
|
|
4
|
+
|
|
5
|
+
WHAT THIS IS FOR
|
|
6
|
+
----------------
|
|
7
|
+
M1/M2 render *previews*: one frame, or a handful of sampled views, to look at.
|
|
8
|
+
M3 renders a **deliverable**: every frame of a range, at the final profile's
|
|
9
|
+
resolution and sample count, so the frames can be encoded into the MP4 that
|
|
10
|
+
leaves the studio. Measured cost on this machine for `watch-commercial` at
|
|
11
|
+
1920x1080 / Cycles / 256 spp is 19.6-41.4 s per frame, so a 450-frame delivery
|
|
12
|
+
is a 3.4 hour process — which is exactly why it must be resumable.
|
|
13
|
+
|
|
14
|
+
WHY THE FRAME LIST IS AN INPUT AND NOT A RANGE
|
|
15
|
+
----------------------------------------------
|
|
16
|
+
"只渲缺失帧" (render only the missing frames) is an acceptance condition, and the
|
|
17
|
+
only way to make it checkable is for the renderer to render EXACTLY the frames it
|
|
18
|
+
was handed. If this module were given `frameStart..frameEnd` and skipped what it
|
|
19
|
+
found on disk, the skip decision would live in Python where the ledger's
|
|
20
|
+
correctness could not be asserted from outside, and a truncated frame left by a
|
|
21
|
+
killed process would be silently kept. The host derives the missing set from the
|
|
22
|
+
frame ledger and passes it; this module renders that set and reports, per frame,
|
|
23
|
+
the artifact it actually wrote.
|
|
24
|
+
|
|
25
|
+
THE NAMING TRAP (measured, not assumed)
|
|
26
|
+
---------------------------------------
|
|
27
|
+
`scene.render.frame_path(frame=f)` computes `<dir>/frame_0001.png`, and
|
|
28
|
+
`bpy.ops.render.render(write_still=True)` writes `<dir>/frame_.png` — the
|
|
29
|
+
predicted name is not the written one. Verified on Blender 5.2.1:
|
|
30
|
+
|
|
31
|
+
frame_path(frame=1) -> /tmp/.../frame_0001.png written: frame_.png
|
|
32
|
+
|
|
33
|
+
So the output path is set EXPLICITLY, with its extension, before every frame, and
|
|
34
|
+
then verified on disk. A renderer that trusted `frame_path` would write every
|
|
35
|
+
frame over the same file and still report success.
|
|
36
|
+
|
|
37
|
+
THE DUPLICATE-EXTENSION TRAP
|
|
38
|
+
----------------------------
|
|
39
|
+
Setting `filepath` to a name that already ends in `.png` while
|
|
40
|
+
`use_file_extension` is true could plausibly produce `frame_0001.png.png`. It does
|
|
41
|
+
not (measured: the listing after two frames is exactly `frame_0001.png`,
|
|
42
|
+
`frame_0042.png`), and the module still verifies the exact expected path rather
|
|
43
|
+
than trusting that.
|
|
44
|
+
|
|
45
|
+
Runs inside Blender's embedded interpreter.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
import json
|
|
49
|
+
import os
|
|
50
|
+
import time
|
|
51
|
+
|
|
52
|
+
import bpy
|
|
53
|
+
|
|
54
|
+
from deepblend_render import apply_render_overrides, find_camera, open_checkpoint, png_dimensions
|
|
55
|
+
from deepblend_util import (
|
|
56
|
+
ActionError,
|
|
57
|
+
Guard,
|
|
58
|
+
error_text,
|
|
59
|
+
frame_file_name,
|
|
60
|
+
frame_path,
|
|
61
|
+
report_progress,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
#: Bytes every frame must carry. A PNG of any real frame is far larger than this;
|
|
65
|
+
#: the check exists to catch a zero-byte file left by a kill between create and
|
|
66
|
+
#: write, which would otherwise be a "frame that exists".
|
|
67
|
+
MIN_FRAME_BYTES = 512
|
|
68
|
+
|
|
69
|
+
#: Blender's PNG signature. A file that does not start with it is not a frame.
|
|
70
|
+
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def append_event(events_path, payload):
|
|
74
|
+
"""Append one JSON line to the durable event journal and make it durable.
|
|
75
|
+
|
|
76
|
+
WHY A JOURNAL AND NOT JUST STDOUT
|
|
77
|
+
---------------------------------
|
|
78
|
+
stdout reaches the host through the subprocess collector, which is an
|
|
79
|
+
in-memory buffer in the host's own process — so it dies with the host. The
|
|
80
|
+
acceptance case is a host that dies mid-render, and the frames on disk are
|
|
81
|
+
the ONLY fact that survives it. This journal is written for the facts disk
|
|
82
|
+
cannot express (which pid is rendering, and when each frame landed), and it is
|
|
83
|
+
flushed and fsynced per line so a kill -9 loses at most the line in flight.
|
|
84
|
+
It is corroboration, never the authority: the frame ledger is re-derived from
|
|
85
|
+
the frames themselves. See `host/lib/frame-ledger.js`.
|
|
86
|
+
"""
|
|
87
|
+
if not events_path:
|
|
88
|
+
return
|
|
89
|
+
try:
|
|
90
|
+
line = json.dumps(payload, ensure_ascii=False) + "\n"
|
|
91
|
+
with open(events_path, "a", encoding="utf-8") as stream:
|
|
92
|
+
stream.write(line)
|
|
93
|
+
stream.flush()
|
|
94
|
+
os.fsync(stream.fileno())
|
|
95
|
+
except Exception as exc: # noqa: BLE001 - a journal that breaks a render is worse than none
|
|
96
|
+
from deepblend_util import eprint
|
|
97
|
+
|
|
98
|
+
eprint("could not append to the event journal: %s" % (error_text(exc),))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def apply_profile(scene, profile, guard):
|
|
102
|
+
"""Apply a whole SceneSpec render profile, and verify it took effect.
|
|
103
|
+
|
|
104
|
+
WHY VERIFY AND NOT JUST SET
|
|
105
|
+
---------------------------
|
|
106
|
+
`view_transform` in particular is rejected outright by some Blender builds
|
|
107
|
+
(the compiler already carries a comment to that effect). Assigning it and
|
|
108
|
+
moving on means a delivery rendered in the wrong colour space reports success
|
|
109
|
+
— the M2 lesson that a silently wrong number is worse than a loud failure.
|
|
110
|
+
Every setting that the profile asked for is read back and compared.
|
|
111
|
+
|
|
112
|
+
Returns the settings actually in force, which travel into the result document.
|
|
113
|
+
"""
|
|
114
|
+
requested = {
|
|
115
|
+
"engine": profile.get("engine"),
|
|
116
|
+
"resolution": profile.get("resolution"),
|
|
117
|
+
"samples": profile.get("samples"),
|
|
118
|
+
"filmTransparent": profile.get("filmTransparent"),
|
|
119
|
+
}
|
|
120
|
+
overrides = apply_render_overrides(
|
|
121
|
+
scene,
|
|
122
|
+
{
|
|
123
|
+
"engine": requested["engine"],
|
|
124
|
+
"width": (requested["resolution"] or [None, None])[0],
|
|
125
|
+
"height": (requested["resolution"] or [None, None])[1],
|
|
126
|
+
"samples": requested["samples"],
|
|
127
|
+
},
|
|
128
|
+
guard,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if requested["filmTransparent"] is not None:
|
|
132
|
+
scene.render.film_transparent = bool(requested["filmTransparent"])
|
|
133
|
+
|
|
134
|
+
color = profile.get("colorManagement") or {}
|
|
135
|
+
view_transform = color.get("viewTransform")
|
|
136
|
+
if view_transform:
|
|
137
|
+
try:
|
|
138
|
+
scene.view_settings.view_transform = view_transform
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
raise ActionError(
|
|
141
|
+
"BLENDER_ENGINE_UNAVAILABLE",
|
|
142
|
+
'the render profile asks for view transform "%s", which this Blender build refuses: %s'
|
|
143
|
+
% (view_transform, error_text(exc)),
|
|
144
|
+
{"requested": view_transform},
|
|
145
|
+
)
|
|
146
|
+
if scene.view_settings.view_transform != view_transform:
|
|
147
|
+
raise ActionError(
|
|
148
|
+
"BLENDER_ENGINE_UNAVAILABLE",
|
|
149
|
+
'the render profile asks for view transform "%s" but Blender kept "%s"'
|
|
150
|
+
% (view_transform, scene.view_settings.view_transform),
|
|
151
|
+
{"requested": view_transform, "applied": scene.view_settings.view_transform},
|
|
152
|
+
)
|
|
153
|
+
look = color.get("look")
|
|
154
|
+
if look:
|
|
155
|
+
try:
|
|
156
|
+
scene.view_settings.look = look
|
|
157
|
+
except Exception:
|
|
158
|
+
guard.warn(
|
|
159
|
+
"SCENE_COMPILER_DECISION",
|
|
160
|
+
'the render profile asks for look "%s", which this Blender build does not offer' % (look,),
|
|
161
|
+
{"requested": look},
|
|
162
|
+
)
|
|
163
|
+
exposure = color.get("exposure")
|
|
164
|
+
if exposure is not None:
|
|
165
|
+
scene.view_settings.exposure = float(exposure)
|
|
166
|
+
|
|
167
|
+
if scene.render.resolution_percentage != 100:
|
|
168
|
+
scene.render.resolution_percentage = 100
|
|
169
|
+
|
|
170
|
+
scene.render.image_settings.file_format = "PNG"
|
|
171
|
+
# 8-bit RGB is what an H.264 delivery consumes; a 16-bit or RGBA frame would
|
|
172
|
+
# triple the bytes on a 13 GiB disk for no visible difference in the MP4.
|
|
173
|
+
try:
|
|
174
|
+
scene.render.image_settings.color_mode = "RGB"
|
|
175
|
+
scene.render.image_settings.color_depth = "8"
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
# Never let a re-render of an existing frame leave a `.blend1`-style backup or
|
|
179
|
+
# a stale file the ledger would then accept.
|
|
180
|
+
scene.render.use_overwrite = True
|
|
181
|
+
|
|
182
|
+
actual = render_config(scene)
|
|
183
|
+
if requested["resolution"] is not None and actual["resolution"] != [
|
|
184
|
+
int(requested["resolution"][0]),
|
|
185
|
+
int(requested["resolution"][1]),
|
|
186
|
+
]:
|
|
187
|
+
raise ActionError(
|
|
188
|
+
"RENDER_PROFILE_MISSING",
|
|
189
|
+
"the render profile asks for %sx%s but the scene is set to %sx%s"
|
|
190
|
+
% (
|
|
191
|
+
requested["resolution"][0], requested["resolution"][1],
|
|
192
|
+
actual["resolution"][0], actual["resolution"][1],
|
|
193
|
+
),
|
|
194
|
+
{"requested": requested["resolution"], "applied": actual["resolution"]},
|
|
195
|
+
)
|
|
196
|
+
return overrides, actual
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def render_config(scene):
|
|
200
|
+
"""The render settings actually in force, for the job's audit record."""
|
|
201
|
+
samples = None
|
|
202
|
+
try:
|
|
203
|
+
if scene.render.engine == "CYCLES":
|
|
204
|
+
samples = int(scene.cycles.samples)
|
|
205
|
+
elif scene.render.engine == "BLENDER_EEVEE":
|
|
206
|
+
samples = int(scene.eevee.taa_render_samples)
|
|
207
|
+
except Exception:
|
|
208
|
+
samples = None
|
|
209
|
+
return {
|
|
210
|
+
"engine": scene.render.engine,
|
|
211
|
+
"resolution": [int(scene.render.resolution_x), int(scene.render.resolution_y)],
|
|
212
|
+
"resolutionPercentage": int(scene.render.resolution_percentage),
|
|
213
|
+
"samples": samples,
|
|
214
|
+
"filmTransparent": bool(scene.render.film_transparent),
|
|
215
|
+
"viewTransform": scene.view_settings.view_transform,
|
|
216
|
+
"look": scene.view_settings.look,
|
|
217
|
+
"exposure": round(float(scene.view_settings.exposure), 6),
|
|
218
|
+
"fps": int(scene.render.fps),
|
|
219
|
+
"fpsBase": int(getattr(scene.render, "fps_base", 1) or 1),
|
|
220
|
+
"frameStart": int(scene.frame_start),
|
|
221
|
+
"frameEnd": int(scene.frame_end),
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def verify_frame(path, expected_width, expected_height):
|
|
226
|
+
"""Prove one written frame is a complete PNG of the expected size.
|
|
227
|
+
|
|
228
|
+
Three facts, all read off the artifact rather than off the render call:
|
|
229
|
+
the PNG signature, the IHDR dimensions, and a size floor. Blender reporting
|
|
230
|
+
success is not evidence — the ledger is built from files, so the file has to
|
|
231
|
+
be the thing that is checked.
|
|
232
|
+
"""
|
|
233
|
+
if not os.path.isfile(path):
|
|
234
|
+
return {"ok": False, "reason": "missing"}
|
|
235
|
+
size = os.path.getsize(path)
|
|
236
|
+
if size < MIN_FRAME_BYTES:
|
|
237
|
+
return {"ok": False, "reason": "truncated", "bytes": size}
|
|
238
|
+
try:
|
|
239
|
+
with open(path, "rb") as stream:
|
|
240
|
+
header = stream.read(33)
|
|
241
|
+
stream.seek(max(0, size - 12))
|
|
242
|
+
tail = stream.read(12)
|
|
243
|
+
except Exception as exc:
|
|
244
|
+
return {"ok": False, "reason": "unreadable: %s" % (error_text(exc),)}
|
|
245
|
+
if len(header) < 24 or header[:8] != PNG_SIGNATURE:
|
|
246
|
+
return {"ok": False, "reason": "not-a-png", "bytes": size}
|
|
247
|
+
width = int.from_bytes(header[16:20], "big")
|
|
248
|
+
height = int.from_bytes(header[20:24], "big")
|
|
249
|
+
if width <= 0 or height <= 0:
|
|
250
|
+
return {"ok": False, "reason": "bad-dimensions", "bytes": size}
|
|
251
|
+
if (expected_width, expected_height) != (None, None) and (width, height) != (expected_width, expected_height):
|
|
252
|
+
return {"ok": False, "reason": "wrong-dimensions", "width": width, "height": height, "bytes": size}
|
|
253
|
+
# A killed process leaves a file with no IEND chunk. This is the cheap
|
|
254
|
+
# structural end-of-file check; full decoding would cost more than the render.
|
|
255
|
+
if tail[-8:-4] != b"IEND":
|
|
256
|
+
return {"ok": False, "reason": "unterminated", "bytes": size}
|
|
257
|
+
return {"ok": True, "bytes": size, "width": width, "height": height}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def render_frames(plan, guard):
|
|
261
|
+
"""Render every frame in ``plan`` and return the per-frame artifact report.
|
|
262
|
+
|
|
263
|
+
``plan`` is a document (not a flag list) for the same reason the view plan is:
|
|
264
|
+
a frame list is a list of records, and N parallel arrays can disagree in
|
|
265
|
+
length. It is validated once, at the top, so a malformed plan is one error
|
|
266
|
+
rather than a half-finished sequence.
|
|
267
|
+
"""
|
|
268
|
+
checkpoint = plan.get("checkpoint")
|
|
269
|
+
if not checkpoint:
|
|
270
|
+
raise ActionError("REVISION_CHECKPOINT_MISSING", "render_frames requires a checkpoint .blend")
|
|
271
|
+
frames = plan.get("frames")
|
|
272
|
+
if not isinstance(frames, list) or len(frames) == 0:
|
|
273
|
+
raise ActionError(
|
|
274
|
+
"BLENDER_SCRIPT_ERROR",
|
|
275
|
+
"render_frames requires a non-empty `frames` list; an empty list would report success "
|
|
276
|
+
"while writing nothing",
|
|
277
|
+
)
|
|
278
|
+
try:
|
|
279
|
+
requested = [int(value) for value in frames]
|
|
280
|
+
except (TypeError, ValueError):
|
|
281
|
+
raise ActionError("BLENDER_SCRIPT_ERROR", "every entry of `frames` must be an integer")
|
|
282
|
+
|
|
283
|
+
output_directory = plan.get("outputDirectory")
|
|
284
|
+
if not output_directory:
|
|
285
|
+
raise ActionError("BLENDER_SCRIPT_ERROR", "render_frames requires an output directory")
|
|
286
|
+
prefix = plan.get("filePrefix") or "frame_"
|
|
287
|
+
padding = int(plan.get("padding") or 4)
|
|
288
|
+
events_path = plan.get("events")
|
|
289
|
+
|
|
290
|
+
os.makedirs(output_directory, exist_ok=True)
|
|
291
|
+
|
|
292
|
+
report_progress("open_checkpoint", 2)
|
|
293
|
+
scene = open_checkpoint(checkpoint)
|
|
294
|
+
|
|
295
|
+
camera_id = plan.get("cameraId")
|
|
296
|
+
camera, _ = find_camera(scene, camera_id)
|
|
297
|
+
scene.camera = camera
|
|
298
|
+
|
|
299
|
+
profile = plan.get("profile") or {}
|
|
300
|
+
report_progress("configure_render", 4)
|
|
301
|
+
overrides, actual = apply_profile(scene, profile, guard)
|
|
302
|
+
expected_width = int(actual["resolution"][0])
|
|
303
|
+
expected_height = int(actual["resolution"][1])
|
|
304
|
+
|
|
305
|
+
# The scene's own range is what the encoder must be told, and it belongs to
|
|
306
|
+
# the spec, not to this invocation's frame list. Report it so the host can
|
|
307
|
+
# check "the MP4 covers the project's range" without re-reading the SceneSpec.
|
|
308
|
+
scene_range = [int(scene.frame_start), int(scene.frame_end)]
|
|
309
|
+
planned_range = plan.get("frameRange")
|
|
310
|
+
if isinstance(planned_range, list) and len(planned_range) == 2 and list(planned_range) != scene_range:
|
|
311
|
+
guard.warn(
|
|
312
|
+
"SCENE_COMPILER_DECISION",
|
|
313
|
+
"the render plan covers frames %s but the checkpoint's scene range is %s; the delivery "
|
|
314
|
+
"manifest will record both" % (planned_range, scene_range),
|
|
315
|
+
{"planned": list(planned_range), "scene": scene_range},
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
entries = []
|
|
319
|
+
written = []
|
|
320
|
+
total = len(requested)
|
|
321
|
+
total_bytes = 0
|
|
322
|
+
for index, frame in enumerate(requested):
|
|
323
|
+
percent = 5 + int(90 * index / total)
|
|
324
|
+
report_progress("frame", percent, {"frame": frame, "index": index, "total": total})
|
|
325
|
+
scene.frame_set(frame)
|
|
326
|
+
path = frame_path(output_directory, frame, prefix, padding)
|
|
327
|
+
# Explicit, absolute, WITH the extension — see the naming trap above.
|
|
328
|
+
scene.render.filepath = path
|
|
329
|
+
started = time.time()
|
|
330
|
+
try:
|
|
331
|
+
bpy.ops.render.render(write_still=True)
|
|
332
|
+
except Exception as exc:
|
|
333
|
+
append_event(events_path, {"type": "frame_failed", "frame": frame, "error": error_text(exc)})
|
|
334
|
+
raise ActionError(
|
|
335
|
+
"BLENDER_ENGINE_UNAVAILABLE",
|
|
336
|
+
"rendering frame %d with engine %s failed: %s" % (frame, scene.render.engine, error_text(exc)),
|
|
337
|
+
{"frame": frame, "engine": scene.render.engine},
|
|
338
|
+
)
|
|
339
|
+
elapsed_ms = int((time.time() - started) * 1000)
|
|
340
|
+
verdict = verify_frame(path, expected_width, expected_height)
|
|
341
|
+
if not verdict.get("ok"):
|
|
342
|
+
append_event(events_path, {"type": "frame_failed", "frame": frame, "verify": verdict})
|
|
343
|
+
raise ActionError(
|
|
344
|
+
"RENDER_NO_OUTPUT",
|
|
345
|
+
"frame %d did not produce a complete PNG at %s (%s)"
|
|
346
|
+
% (frame, path, verdict.get("reason")),
|
|
347
|
+
{"frame": frame, "output": path, "verify": verdict},
|
|
348
|
+
)
|
|
349
|
+
entry = {
|
|
350
|
+
"frame": frame,
|
|
351
|
+
"path": path,
|
|
352
|
+
"file": frame_file_name(frame, prefix, padding),
|
|
353
|
+
"bytes": verdict["bytes"],
|
|
354
|
+
"width": verdict["width"],
|
|
355
|
+
"height": verdict["height"],
|
|
356
|
+
"ms": elapsed_ms,
|
|
357
|
+
}
|
|
358
|
+
entries.append(entry)
|
|
359
|
+
written.append(frame)
|
|
360
|
+
total_bytes += verdict["bytes"]
|
|
361
|
+
# Written AFTER the file is verified on disk, so the journal never claims
|
|
362
|
+
# a frame the ledger would call missing.
|
|
363
|
+
append_event(events_path, {"type": "frame", "frame": frame, "bytes": verdict["bytes"], "ms": elapsed_ms})
|
|
364
|
+
|
|
365
|
+
report_progress("frames_done", 97, {"rendered": len(written)})
|
|
366
|
+
append_event(events_path, {"type": "frames_done", "rendered": written})
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
"renderKind": "frame-sequence",
|
|
370
|
+
"profileName": plan.get("profileName"),
|
|
371
|
+
"checkpoint": checkpoint,
|
|
372
|
+
"cameraId": camera.get("deepblend_id") or camera.name,
|
|
373
|
+
"cameraName": camera.name,
|
|
374
|
+
"outputDirectory": output_directory,
|
|
375
|
+
"filePrefix": prefix,
|
|
376
|
+
"padding": padding,
|
|
377
|
+
"frames": entries,
|
|
378
|
+
"renderedFrames": written,
|
|
379
|
+
"renderedCount": len(written),
|
|
380
|
+
"totalBytes": total_bytes,
|
|
381
|
+
"sceneFrameRange": scene_range,
|
|
382
|
+
"renderConfig": actual,
|
|
383
|
+
"overrides": overrides,
|
|
384
|
+
"requestedProfile": {
|
|
385
|
+
"engine": profile.get("engine"),
|
|
386
|
+
"resolution": profile.get("resolution"),
|
|
387
|
+
"samples": profile.get("samples"),
|
|
388
|
+
},
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
__all__ = [
|
|
393
|
+
"Guard",
|
|
394
|
+
"MIN_FRAME_BYTES",
|
|
395
|
+
"append_event",
|
|
396
|
+
"frame_file_name",
|
|
397
|
+
"frame_path",
|
|
398
|
+
"png_dimensions",
|
|
399
|
+
"render_config",
|
|
400
|
+
"render_frames",
|
|
401
|
+
"verify_frame",
|
|
402
|
+
]
|