Skip to content
This repository was archived by the owner on Jul 17, 2026. It is now read-only.

Commit 1b43ce7

Browse files
committed
1 parent 5aac4b8 commit 1b43ce7

4 files changed

Lines changed: 483 additions & 1 deletion

File tree

llvmexpr/llvmexpr.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2172,7 +2172,9 @@ class Compiler {
21722172
llvm::Value* clamped_f =
21732173
createBinaryIntrinsicCall(llvm::Intrinsic::minnum, temp, max_f);
21742174

2175-
final_val = builder.CreateFPToUI(clamped_f, builder.getInt32Ty());
2175+
llvm::Value* rounded_f =
2176+
createUnaryIntrinsicCall(llvm::Intrinsic::roundeven, clamped_f);
2177+
final_val = builder.CreateFPToUI(rounded_f, builder.getInt32Ty());
21762178

21772179
if (bpp == 1) {
21782180
final_val = builder.CreateTrunc(final_val, builder.getInt8Ty());

pytest.ini

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
; Copyright (C) 2025 yuygfgg
2+
;
3+
; This file is part of Vapoursynth-llvmexpr.
4+
;
5+
; Vapoursynth-llvmexpr is free software: you can redistribute it and/or modify
6+
; it under the terms of the GNU General Public License as published by
7+
; the Free Software Foundation, either version 3 of the License, or
8+
; (at your option) any later version.
9+
;
10+
; Vapoursynth-llvmexpr is distributed in the hope that it will be useful,
11+
; but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
; GNU General Public License for more details.
14+
;
15+
; You should have received a copy of the GNU General Public License
16+
; along with Vapoursynth-llvmexpr. If not, see <https://www.gnu.org/licenses/>.
17+
18+
[pytest]
19+
addopts = -p no:vsengine

tests/test_expr.py

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
"""
2+
Copyright (C) 2025 sgt0
3+
Copyright (C) 2025 yuygfgg
4+
5+
This file is part of Vapoursynth-llvmexpr.
6+
7+
This file is derived from and has been modified from code
8+
originally contributed by sgt0 in a pull request to the akarin-vapoursynth-plugin
9+
project (https://github.com/Jaded-Encoding-Thaumaturgy/akarin-vapoursynth-plugin/pull/12),
10+
which was licensed under LGPLv3.
11+
12+
As Vapoursynth-llvmexpr is licensed under the GNU General Public License v3, this
13+
modified file is also distributed under the same license.
14+
15+
Vapoursynth-llvmexpr is free software: you can redistribute it and/or modify
16+
it under the terms of the GNU General Public License as published by
17+
the Free Software Foundation, either version 3 of the License, or
18+
(at your option) any later version.
19+
20+
Vapoursynth-llvmexpr is distributed in the hope that it will be useful,
21+
but WITHOUT ANY WARRANTY; without even the implied warranty of
22+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23+
GNU General Public License for more details.
24+
25+
You should have received a copy of the GNU General Public License
26+
along with Vapoursynth-llvmexpr. If not, see <https://www.gnu.org/licenses/>.
27+
"""
28+
29+
import pytest
30+
import vapoursynth as vs
31+
32+
core = vs.core
33+
34+
35+
@pytest.mark.parametrize(
36+
"input_format, a, b, expr, expected",
37+
[
38+
(vs.GRAYS, 2.0, 3.0, "x y +", 5.0),
39+
(vs.GRAYS, 7.0, 5.0, "x y -", 2.0),
40+
(vs.GRAYS, 4.0, 2.5, "x y *", 10.0),
41+
(vs.GRAYS, 7.5, 2.5, "x y /", 3.0),
42+
(vs.GRAYS, 5.25, 1.0, "x 1.0 %", 0.25),
43+
],
44+
)
45+
def test_arithmetic(
46+
input_format: int, a: float, b: float, expr: str, expected: float
47+
) -> None:
48+
c1 = core.std.BlankClip(format=input_format, color=a)
49+
c2 = core.std.BlankClip(format=input_format, color=b)
50+
res = core.llvmexpr.Expr([c1, c2], expr, vs.GRAYS)
51+
assert res.get_frame(0)[0][0, 0] == pytest.approx(expected)
52+
53+
54+
@pytest.mark.parametrize(
55+
"a, b, expr, expected",
56+
[
57+
(3.0, 2.0, "x y >", 1.0),
58+
(2.0, 3.0, "x y <", 1.0),
59+
(3.0, 3.0, "x y =", 1.0),
60+
(3.0, 3.0, "x y >=", 1.0),
61+
(2.0, 3.0, "x y <=", 1.0),
62+
(1.0, 0.0, "x y and", 0.0),
63+
(1.0, 0.0, "x y or", 1.0),
64+
(1.0, 1.0, "x y xor", 0.0),
65+
(0.0, 0.0, "x not", 1.0),
66+
(-2, -3, "x y and", 0.0),
67+
(-2, -1, "x y or", 0.0),
68+
(-2, -1, "x y xor", 0.0),
69+
(-2, 3, "x y xor", 1.0),
70+
],
71+
)
72+
def test_comparison_and_logical(a: float, b: float, expr: str, expected: float) -> None:
73+
c1 = core.std.BlankClip(format=vs.GRAYS, color=a)
74+
c2 = core.std.BlankClip(format=vs.GRAYS, color=b)
75+
res = core.llvmexpr.Expr([c1, c2], expr, vs.GRAYS)
76+
assert res.get_frame(0)[0][0, 0] == pytest.approx(expected)
77+
78+
79+
@pytest.mark.parametrize(
80+
"input_format, input_value, expr, expected",
81+
[
82+
(vs.GRAY8, 0, "0 exp", 1.0),
83+
(vs.GRAY8, 0, "x exp", 1.0),
84+
(vs.GRAY8, 1, "x exp", 2.71828),
85+
(vs.GRAYS, 0.5, "x exp", 1.64872),
86+
],
87+
)
88+
def test_exp(input_format: int, input_value: int, expr: str, expected: float) -> None:
89+
clip = core.std.BlankClip(format=input_format, color=input_value)
90+
result = core.llvmexpr.Expr(clip, expr, vs.GRAYS)
91+
assert result.get_frame(0)[0][0, 0] == pytest.approx(expected)
92+
93+
94+
@pytest.mark.parametrize(
95+
"input_format, input_value, expr, expected",
96+
[
97+
(vs.GRAY8, 0, "0 log", float("-inf")),
98+
(
99+
vs.GRAY8,
100+
0,
101+
"x log",
102+
float("-inf"),
103+
), # differs from std.Expr's -87.3365478515625
104+
(vs.GRAY8, 1, "x log", 0),
105+
(vs.GRAYS, 7.38905, "x log", 2),
106+
],
107+
)
108+
def test_log(input_format: int, input_value: int, expr: str, expected: float) -> None:
109+
clip = core.std.BlankClip(format=input_format, color=input_value)
110+
result = core.llvmexpr.Expr(clip, expr, vs.GRAYS)
111+
assert result.get_frame(0)[0][0, 0] == pytest.approx(expected)
112+
113+
114+
@pytest.mark.parametrize(
115+
"val, expr, expected",
116+
[
117+
(1.9, "x floor", 1.0),
118+
(1.1, "x ceil", 2.0),
119+
(2.49, "x round", 2.0),
120+
(2.5, "x round", 3.0),
121+
(-2.9, "x trunc", -2.0),
122+
(2.9, "x abs", 2.9),
123+
(3.0, "x neg", -3.0),
124+
(-5.0, "x sgn", -1.0),
125+
(0.0, "x sgn", 0.0),
126+
(7.0, "x sgn", 1.0),
127+
(2.0, "x -3 copysign", -2.0),
128+
(2.0, "2 3 4 fma", 10.0),
129+
],
130+
)
131+
def test_rounding_and_misc(val: float, expr: str, expected: float) -> None:
132+
c = core.std.BlankClip(format=vs.GRAYS, color=val)
133+
res = core.llvmexpr.Expr(c, expr, vs.GRAYS)
134+
assert res.get_frame(0)[0][0, 0] == pytest.approx(expected)
135+
136+
137+
@pytest.mark.parametrize("input_format", [vs.GRAY8, vs.GRAY16, vs.GRAYS])
138+
@pytest.mark.parametrize(
139+
"expr, expected",
140+
[
141+
("x 1.5 pow", 0.0),
142+
],
143+
)
144+
def test_pow(input_format: int, expr: str, expected: float) -> None:
145+
clip = core.std.BlankClip(format=input_format, color=0)
146+
result = core.llvmexpr.Expr(clip, expr, vs.GRAYS)
147+
assert result.get_frame(0)[0][0, 0] == pytest.approx(expected)
148+
149+
150+
@pytest.mark.parametrize(
151+
"input_format, input_value, expr, expected",
152+
[
153+
(vs.GRAY8, 0, "x sin", 0),
154+
(vs.GRAY8, 1, "x sin", 0.8414709568023682),
155+
(vs.GRAY8, 2, "x sin", 0.9092974066734314),
156+
],
157+
)
158+
def test_sin(input_format: int, input_value: int, expr: str, expected: float) -> None:
159+
clip = core.std.BlankClip(format=input_format, color=input_value)
160+
result = core.llvmexpr.Expr(clip, expr, vs.GRAYS)
161+
assert result.get_frame(0)[0][0, 0] == pytest.approx(expected)
162+
163+
164+
def test_gh_11() -> None:
165+
clip = core.std.BlankClip(format=vs.GRAY8, color=0)
166+
result = core.llvmexpr.Expr(clip, "x 128 / 0.86 pow 255 *")
167+
assert result.get_frame(0)[0][0, 0] == pytest.approx(6.122468756907559e-31)
168+
169+
clip = core.std.BlankClip(format=vs.GRAY16, color=0)
170+
result = core.llvmexpr.Expr(clip, "x 32768 / 0.86 pow 65535 *")
171+
assert result.get_frame(0)[0][0, 0] == pytest.approx(1.5734745330615421e-28)
172+
173+
174+
@pytest.mark.parametrize(
175+
"val, min_v, max_v, expected_max, expected_min",
176+
[
177+
(5.0, 2.0, 8.0, 8.0, 2.0),
178+
(1.0, 2.0, 8.0, 2.0, 1.0),
179+
],
180+
)
181+
def test_min_max_clip(
182+
val: float, min_v: float, max_v: float, expected_max: float, expected_min: float
183+
) -> None:
184+
c = core.std.BlankClip(format=vs.GRAYS, color=val)
185+
res_max = core.llvmexpr.Expr(c, "x 3 max", vs.GRAYS)
186+
res_min = core.llvmexpr.Expr(c, "x 3 min", vs.GRAYS)
187+
res_clip = core.llvmexpr.Expr(c, f"x {min_v} {max_v} clip", vs.GRAYS)
188+
assert res_max.get_frame(0)[0][0, 0] == pytest.approx(max(val, 3.0))
189+
assert res_min.get_frame(0)[0][0, 0] == pytest.approx(min(val, 3.0))
190+
assert res_clip.get_frame(0)[0][0, 0] == pytest.approx(min(max(val, min_v), max_v))
191+
192+
193+
@pytest.mark.parametrize(
194+
"a, b, expr, expected",
195+
[
196+
(5.9, 1.0, "x y bitand", 1.0),
197+
(5.0, 2.0, "x y bitor", 7.0),
198+
(5.0, 1.0, "x y bitxor", 4.0),
199+
(
200+
5.0,
201+
0.0,
202+
"x bitnot",
203+
(
204+
float(2**32 - 1 - 5)
205+
if vs.core.core_version.release_major
206+
>= 64 # pyright: ignore[reportOperatorIssue]
207+
else float(~5 & 0xFFFFFFFF)
208+
),
209+
),
210+
],
211+
)
212+
def test_bitwise(a: float, b: float, expr: str, expected: float) -> None:
213+
c1 = core.std.BlankClip(format=vs.GRAYS, color=a)
214+
c2 = core.std.BlankClip(format=vs.GRAYS, color=b)
215+
res = core.llvmexpr.Expr([c1, c2], expr, vs.GRAYS)
216+
# bitnot result depends on implementation width; for generality, compare masked
217+
out = res.get_frame(0)[0][0, 0]
218+
if "bitnot" in expr:
219+
assert int(out) & 0xFFFFFFFF == (~int(a)) & 0xFFFFFFFF
220+
else:
221+
assert out == pytest.approx(expected)
222+
223+
224+
def test_stack_manipulation() -> None:
225+
c = core.std.BlankClip(format=vs.GRAYS, color=3.0)
226+
res_dup = core.llvmexpr.Expr(c, "x dup *", vs.GRAYS)
227+
assert res_dup.get_frame(0)[0][0, 0] == pytest.approx(9.0)
228+
c0 = core.std.BlankClip(format=vs.GRAYS, color=0.0)
229+
res_swap = core.llvmexpr.Expr(c0, "2 5 swap -", vs.GRAYS)
230+
assert res_swap.get_frame(0)[0][0, 0] == pytest.approx(3.0)
231+
res_drop = core.llvmexpr.Expr(c0, "1 2 3 drop2", vs.GRAYS)
232+
assert res_drop.get_frame(0)[0][0, 0] == pytest.approx(1.0)
233+
res_sort = core.llvmexpr.Expr(c0, "3 1 2 sort3 drop2", vs.GRAYS)
234+
assert res_sort.get_frame(0)[0][0, 0] == pytest.approx(1.0)
235+
236+
237+
def test_named_variables_and_loop_power() -> None:
238+
c = core.std.BlankClip(format=vs.GRAYS, color=2.0)
239+
y = core.std.BlankClip(format=vs.GRAYS, color=4.0)
240+
expr = "x base! 1 result! y counter! #loop result@ base@ * result! counter@ 1 - counter! counter@ loop# result@"
241+
res = core.llvmexpr.Expr([c, y], expr, vs.GRAYS)
242+
assert res.get_frame(0)[0][0, 0] == pytest.approx(16.0)
243+
244+
245+
def test_constants_and_coords() -> None:
246+
c0 = core.std.BlankClip(format=vs.GRAYS, color=0.0, width=3, height=2)
247+
res_pi = core.llvmexpr.Expr(c0, "pi", vs.GRAYS)
248+
assert res_pi.get_frame(0)[0][0, 0] == pytest.approx(3.14159265, rel=1e-6)
249+
res_N = core.llvmexpr.Expr(c0, "N", vs.GRAYS)
250+
assert res_N.get_frame(3)[0][0, 0] == pytest.approx(3.0)
251+
res_wh = core.llvmexpr.Expr(c0, "width height +", vs.GRAYS)
252+
assert res_wh.get_frame(0)[0][0, 0] == pytest.approx(5.0)
253+
res_X = core.llvmexpr.Expr(c0, "X", vs.GRAYS)
254+
res_Y = core.llvmexpr.Expr(c0, "Y", vs.GRAYS)
255+
fX = res_X.get_frame(0)
256+
fY = res_Y.get_frame(0)
257+
assert fX[0][1, 2] == pytest.approx(2.0)
258+
assert fY[0][1, 2] == pytest.approx(1.0)
259+
260+
261+
def test_conditional_ternary() -> None:
262+
c = core.std.BlankClip(format=vs.GRAYS, color=10.0)
263+
res = core.llvmexpr.Expr(c, "x 5 > 1 0 ?", vs.GRAYS)
264+
assert res.get_frame(0)[0][0, 0] == pytest.approx(1.0)
265+
266+
267+
def test_pixel_access_static_and_dynamic() -> None:
268+
base = core.std.BlankClip(format=vs.GRAYS, color=0.0, width=4, height=2)
269+
ramp = core.llvmexpr.Expr(base, "X", vs.GRAYS)
270+
src = core.std.BlankClip(format=vs.GRAYS, color=99.0, width=4, height=2)
271+
expr_rel = "y[-1,0]"
272+
res_rel = core.llvmexpr.Expr([src, ramp], expr_rel, vs.GRAYS)
273+
f = res_rel.get_frame(0)
274+
assert f[0][0, 2] == pytest.approx(1.0)
275+
expr_abs = "1 1 y[]"
276+
res_abs = core.llvmexpr.Expr([src, ramp], expr_abs, vs.GRAYS)
277+
assert res_abs.get_frame(0)[0][0, 0] == pytest.approx(1.0)
278+
279+
280+
def test_frame_property_access() -> None:
281+
c = core.std.BlankClip(format=vs.GRAYS, color=0.0)
282+
c = core.std.SetFrameProps(c, _TestProp=0.25)
283+
res = core.llvmexpr.Expr(c, "x._TestProp", vs.GRAYS)
284+
assert res.get_frame(0)[0][0, 0] == pytest.approx(0.25)
285+
286+
287+
def test_direct_output_write_and_exit() -> None:
288+
base = core.std.BlankClip(format=vs.GRAYS, color=0.0, width=4, height=4)
289+
expr = "X 1 = Y 2 = and 5 1 2 @[] ^exit^ 0 ?"
290+
res = core.llvmexpr.Expr(base, expr, vs.GRAYS)
291+
fr = res.get_frame(0)
292+
assert fr[0][2, 1] == pytest.approx(5.0)
293+
assert fr[0][0, 0] == pytest.approx(0.0)

0 commit comments

Comments
 (0)