Skip to content

Commit 25db191

Browse files
committed
convert: Make sure no real closures exist.
That should prevent reference cycles. Hopefully.
1 parent b5fcf5f commit 25db191

File tree

3 files changed

+103
-47
lines changed

3 files changed

+103
-47
lines changed

vsengine/_helpers.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ def wrap_variable_size(
4949
if node.format is not None and node.width != 0 and node.height != 0:
5050
return func(node)
5151

52-
_node_cache = {}
5352
def _do_resize(f: vs.VideoFrame) -> vs.VideoNode:
5453
# Resize the node to make them assume a specific format.
5554
# As the node should aready have this format, this should be a no-op.
5655
return func(node.resize.Point(format=f.format, width=f.width, height=f.height))
5756

57+
_node_cache = {}
5858
def _assume_format(n: int, f: vs.VideoFrame) -> vs.VideoNode:
5959
nonlocal _node_cache
6060
selector = (int(f.format), f.width, f.height)
@@ -75,6 +75,10 @@ def _assume_format(n: int, f: vs.VideoFrame) -> vs.VideoNode:
7575

7676
return wrapped
7777

78-
evaled = vs.core.std.FrameEval(node, _assume_format, node)
79-
return evaled.resize.Point(format=force_assumed_format)
78+
# This clip must not become part of the closure,
79+
# or otherwise we risk cyclic references.
80+
return (
81+
node.std.FrameEval(_assume_format, [node], [node])
82+
.resize.Point(format=force_assumed_format)
83+
)
8084

vsengine/convert.py

Lines changed: 94 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,69 @@ def yuv_heuristic(width: int, height: int) -> t.Mapping[str, str]:
5353
return result
5454

5555

56+
# Move this function out of the closure to avoid capturing clip.
57+
def _convert_yuv(
58+
c: vs.VideoNode,
59+
*,
60+
core: vs.Core,
61+
real_rgb24: vs.VideoFormat,
62+
default_args: t.Dict[str, t.Any],
63+
scaler: t.Union[str, t.Callable[..., vs.VideoNode]]
64+
):
65+
# We make yuv_heuristic not configurable so the heuristic
66+
# will be shared across projects.
67+
#
68+
# In my opinion, this is a quirk that should be shared.
69+
70+
args = {
71+
**yuv_heuristic(c.width, c.height),
72+
**default_args
73+
}
74+
75+
if c.format.subsampling_w != 0 or c.format.subsampling_h != 0:
76+
# To be clear, scaler should always be a string.
77+
# Being able to provide a callable just makes testing args easier.
78+
resizer = getattr(core.resize, scaler) if isinstance(scaler, str) else scaler
79+
else:
80+
# In this case we only do cs transforms, point resize is more then enough.
81+
resizer = core.resize.Point
82+
83+
# Keep bitdepth so we can dither futher down in the RGB part.
84+
return resizer(
85+
c,
86+
format=real_rgb24.replace(bits_per_sample=c.format.bits_per_sample),
87+
**args
88+
)
89+
90+
91+
# Move this function out of the closure to avoid capturing clip.
92+
def _actually_resize(
93+
c: vs.VideoNode,
94+
*,
95+
core: vs.Core,
96+
convert_yuv: t.Callable[[vs.VideoNode], vs.VideoNode],
97+
target_rgb: vs.VideoFormat
98+
) -> vs.VideoNode:
99+
# Converting to YUV is a little bit more complicated,
100+
# so I extracted it to its own function.
101+
if c.format.color_family == vs.YUV:
102+
c = convert_yuv(c)
103+
104+
# Defaulting prefer_props to True makes resizing choke
105+
# on GRAY clips.
106+
if c.format == vs.GRAY:
107+
c = c.std.RemoveFrameProps("_Matrix")
108+
109+
# Actually perform the format conversion on a non-subsampled clip.
110+
if c.format.color_family != vs.RGB or c.format.bits_per_sample != target_rgb.bits_per_sample:
111+
c = core.resize.Point(
112+
c,
113+
format=target_rgb
114+
)
115+
116+
return c
117+
118+
56119
def to_rgb(
57120
clip: vs.VideoNode,
58121
env: t.Optional[EnvironmentTypes] = None,
@@ -61,7 +124,7 @@ def to_rgb(
61124
bits_per_sample: int = 8,
62125

63126
# Input: YUV
64-
scaler: t.Union[str, t.Callable[..., vs.VideoNode]] = "Spline36",
127+
scaler: t.Union[str, t.Callable[..., vs.VideoNode]] = "Bicubic",
65128
default_matrix: t.Optional[str] = None,
66129
default_transfer: t.Optional[str] = None,
67130
default_primaries: t.Optional[str] = None,
@@ -78,59 +141,46 @@ def to_rgb(
78141
:param default_*: Manually override the defaults predicted by the heuristics.
79142
:param yuv_heuristic: The heuristic function that takes the frame size and returns a set of yuv-metadata. (For test purposes)
80143
"""
144+
145+
# This function does a lot.
146+
# This is why there are so many comments.
147+
148+
default_args = {}
149+
if default_matrix is not None:
150+
default_args["matrix_in_s"] = default_matrix
151+
if default_transfer is not None:
152+
default_args["transfer_in_s"] = default_transfer
153+
if default_primaries is not None:
154+
default_args["primaries_in_s"] = default_primaries
155+
if default_range is not None:
156+
default_args["range_in_s"] = default_range
157+
if default_chromaloc is not None:
158+
default_args["chromaloc_in_s"] = default_chromaloc
159+
81160
with use_inline("to_rgb", env):
82161
core = vs.core.core
83162
real_rgb24 = core.get_video_format(vs.RGB24)
84163
target_rgb = real_rgb24.replace(bits_per_sample=bits_per_sample)
85164

86-
def _convert_yuv(c: vs.VideoNode):
87-
# We make yuv_heuristic not configurable so the heuristic
88-
#
89-
args = {
90-
**yuv_heuristic(c.width, c.height)
91-
}
92-
93-
# Override heuristics with default values if specified.
94-
if default_matrix is not None:
95-
args["matrix_in_s"] = default_matrix
96-
if default_transfer is not None:
97-
args["transfer_in_s"] = default_transfer
98-
if default_primaries is not None:
99-
args["primaries_in_s"] = default_primaries
100-
if default_range is not None:
101-
args["range_in_s"] = default_range
102-
if default_chromaloc is not None:
103-
args["chromaloc_in_s"] = default_chromaloc
104-
105-
# Detect subsampling
106-
if clip.format.subsampling_w != 0 or clip.format.subsampling_h != 0:
107-
# Allowing to manually specify a scaler function allows for testing of the args.
108-
resizer = getattr(core.resize, scaler) if isinstance(scaler, str) else scaler
109-
else:
110-
resizer = core.resize.Point
111-
112-
return resizer(
113-
c,
114-
format=target_rgb,
115-
**args
165+
# This avoids capturing `clip` in a closure creating a self-reference.
166+
convert_yuv = functools.partial(
167+
_convert_yuv,
168+
core=core,
169+
real_rgb24=real_rgb24,
170+
default_args=default_args,
171+
scaler=scaler
116172
)
117173

118-
def _actually_resize(c: vs.VideoNode) -> vs.VideoNode:
119-
if c.format.color_family == vs.YUV:
120-
c = _convert_yuv(c)
121-
122-
if c.format == vs.GRAY:
123-
c = c.std.RemoveFrameProps("_Matrix")
124-
125-
if c.format.color_family != vs.RGB or c.format.bits_per_sample != bits_per_sample:
126-
c = core.resize.Point(c, format=target_rgb)
127-
128-
return c
174+
actually_resize = functools.partial(
175+
_actually_resize,
176+
core=core,
177+
target_rgb=target_rgb,
178+
convert_yuv=convert_yuv
179+
)
129180

130-
with use_inline("to_rgb", env):
131181
return wrap_variable_size(
132182
clip,
133183
force_assumed_format=target_rgb,
134-
func=_actually_resize
184+
func=actually_resize
135185
)
136186

vsengine/loops.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ def attach(self) -> None:
4848
def detach(self) -> None:
4949
"""
5050
Called when another event-loop should take over.
51+
52+
For example, when you restarting your application.
5153
"""
5254
...
5355

0 commit comments

Comments
 (0)