Skip to content

Commit 3a1f46c

Browse files
committed
Add OpenGL multi-open OCIO test and clear GL errors
Add a focused regression test for OpenGL + OCIO with multiple images: a new Python runner script (src/imiv/tools/imiv_opengl_multiopen_ocio_regression.py) that launches imiv with two copies of a source image, captures layout/state/screenshot, and scans the runtime log for known error patterns. Integrate the test into CTest (src/imiv/CMakeLists.txt) with labels and timeout, and document the test in docs/README (src/doc/imiv_tests.rst, src/imiv/README.md). Also introduce clear_gl_error_queue() in the OpenGL renderer (src/imiv/imiv_renderer_opengl.cpp) and call it at several points before texture uploads and draw setup to flush stale GL errors and reduce spurious "OpenGL OCIO preview draw failed" runtime failures during startup. Signed-off-by: Vlad (Kuzmin) Erium <libalias@gmail.com> Signed-off-by: Vlad <shaamaan@gmail.com>
1 parent 3f77811 commit 3a1f46c

File tree

5 files changed

+220
-0
lines changed

5 files changed

+220
-0
lines changed

src/doc/imiv_tests.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,9 @@ Recent focused GUI regressions in `src/imiv/tools/` include:
243243
* `imiv_view_recipe_regression.py`
244244
per-view recipe isolation for exposure, gamma, offset, interpolation, and
245245
OCIO state with multiple image views open.
246+
* `imiv_opengl_multiopen_ocio_regression.py`
247+
OpenGL multi-image startup with OCIO enabled, with a hard failure if the
248+
runtime log reports `OpenGL OCIO preview draw failed`.
246249
* `imiv_save_selection_regression.py`
247250
GUI-driven `Save Selection As...` crop export, including selected ROI and
248251
orientation-baked CPU output validation.

src/imiv/CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,6 +1289,22 @@ if (TARGET imiv
12891289
imiv_opengl_smoke_regression PROPERTIES
12901290
LABELS "imiv;imiv_opengl;gui"
12911291
TIMEOUT 180)
1292+
1293+
add_test (
1294+
NAME imiv_opengl_multiopen_ocio_regression
1295+
COMMAND
1296+
"${Python3_EXECUTABLE}"
1297+
"${CMAKE_CURRENT_SOURCE_DIR}/tools/imiv_opengl_multiopen_ocio_regression.py"
1298+
--bin "$<TARGET_FILE:imiv>"
1299+
--cwd "$<TARGET_FILE_DIR:imiv>"
1300+
--backend opengl
1301+
--env-script "${CMAKE_BINARY_DIR}/imiv_env.sh"
1302+
--out-dir "${CMAKE_BINARY_DIR}/imiv_captures/opengl_multiopen_ocio_regression"
1303+
--open "${PROJECT_SOURCE_DIR}/ASWF/logos/openimageio-stacked-gradient.png")
1304+
set_tests_properties (
1305+
imiv_opengl_multiopen_ocio_regression PROPERTIES
1306+
LABELS "imiv;imiv_opengl;imiv_ocio;gui"
1307+
TIMEOUT 180)
12921308
elseif (_imiv_renderer_is_metal)
12931309
add_test (
12941310
NAME imiv_metal_screenshot_regression

src/imiv/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,17 @@ python3 src/imiv/tools/imiv_image_list_center_regression.py \
165165
--out-dir build/imiv_captures/image_list_center_regression
166166
```
167167

168+
Focused OpenGL multi-open OCIO regression:
169+
170+
```bash
171+
python3 src/imiv/tools/imiv_opengl_multiopen_ocio_regression.py \
172+
--bin build/bin/imiv \
173+
--cwd build/bin \
174+
--backend opengl \
175+
--env-script build/imiv_env.sh \
176+
--out-dir build/imiv_captures/opengl_multiopen_ocio_regression
177+
```
178+
168179
Focused folder-open regression:
169180

170181
```bash

src/imiv/imiv_renderer_opengl.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,13 @@ namespace Imiv {
138138

139139
namespace {
140140

141+
void
142+
clear_gl_error_queue()
143+
{
144+
while (glGetError() != GL_NO_ERROR) {
145+
}
146+
}
147+
141148
struct RendererTextureBackendState {
142149
GLuint source_texture = 0;
143150
GLuint preview_linear_texture = 0;
@@ -535,6 +542,7 @@ struct RendererBackendState {
535542
glBindTexture(target, texture_id);
536543
set_ocio_texture_parameters(target, filter);
537544
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
545+
clear_gl_error_queue();
538546
const float* values = blueprint.values.empty()
539547
? nullptr
540548
: blueprint.values.data();
@@ -1142,6 +1150,7 @@ void main()
11421150
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
11431151
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
11441152
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
1153+
clear_gl_error_queue();
11451154
glTexImage2D(GL_TEXTURE_2D, 0, upload.internal_format, width, height, 0,
11461155
upload.format, upload.type, nullptr);
11471156
GLenum err = glGetError();
@@ -1192,6 +1201,7 @@ void main()
11921201
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
11931202
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
11941203
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
1204+
clear_gl_error_queue();
11951205
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA,
11961206
GL_FLOAT, nullptr);
11971207
const GLenum err = glGetError();
@@ -1230,6 +1240,7 @@ void main()
12301240
glViewport(0, 0, texture_state.width, texture_state.height);
12311241
glDisable(GL_BLEND);
12321242
glDisable(GL_DEPTH_TEST);
1243+
clear_gl_error_queue();
12331244
glUseProgram(state.basic_preview.program);
12341245
glBindVertexArray(state.basic_preview.fullscreen_triangle_vao);
12351246
glActiveTexture(GL_TEXTURE0);
@@ -1304,6 +1315,7 @@ void main()
13041315
glViewport(0, 0, texture_state.width, texture_state.height);
13051316
glDisable(GL_BLEND);
13061317
glDisable(GL_DEPTH_TEST);
1318+
clear_gl_error_queue();
13071319
glUseProgram(state.ocio_preview.program);
13081320
glBindVertexArray(state.basic_preview.fullscreen_triangle_vao);
13091321

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#!/usr/bin/env python3
2+
"""Regression check for OpenGL OCIO preview with multiple startup images."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import json
8+
import os
9+
import shlex
10+
import shutil
11+
import subprocess
12+
import sys
13+
from pathlib import Path
14+
15+
16+
ERROR_PATTERNS = (
17+
"error: imiv exited with code",
18+
"OpenGL preview draw failed",
19+
"OpenGL OCIO preview draw failed",
20+
)
21+
22+
23+
def _repo_root() -> Path:
24+
return Path(__file__).resolve().parents[3]
25+
26+
27+
def _default_binary(repo_root: Path) -> Path:
28+
candidates = [
29+
repo_root / "build_u" / "bin" / "imiv",
30+
repo_root / "build" / "bin" / "imiv",
31+
repo_root / "build_u" / "src" / "imiv" / "imiv",
32+
repo_root / "build" / "src" / "imiv" / "imiv",
33+
repo_root / "build" / "Debug" / "imiv.exe",
34+
repo_root / "build" / "Release" / "imiv.exe",
35+
]
36+
for candidate in candidates:
37+
if candidate.exists():
38+
return candidate
39+
return candidates[0]
40+
41+
42+
def _load_env_from_script(script_path: Path) -> dict[str, str]:
43+
env = dict(os.environ)
44+
if not script_path.exists() or shutil.which("bash") is None:
45+
return env
46+
47+
quoted = shlex.quote(str(script_path))
48+
proc = subprocess.run(
49+
["bash", "-lc", f"source {quoted} >/dev/null 2>&1; env -0"],
50+
check=True,
51+
stdout=subprocess.PIPE,
52+
)
53+
for item in proc.stdout.split(b"\0"):
54+
if not item:
55+
continue
56+
key, _, value = item.partition(b"=")
57+
if not key:
58+
continue
59+
env[key.decode("utf-8", errors="ignore")] = value.decode(
60+
"utf-8", errors="ignore"
61+
)
62+
return env
63+
64+
65+
def main() -> int:
66+
repo_root = _repo_root()
67+
runner = repo_root / "src" / "imiv" / "tools" / "imiv_gui_test_run.py"
68+
default_env_script = repo_root / "build_u" / "imiv_env.sh"
69+
default_out_dir = (
70+
repo_root / "build_u" / "imiv_captures" / "opengl_multiopen_ocio_regression"
71+
)
72+
default_image = repo_root / "ASWF" / "logos" / "openimageio-stacked-gradient.png"
73+
74+
ap = argparse.ArgumentParser(description=__doc__)
75+
ap.add_argument("--bin", default=str(_default_binary(repo_root)), help="imiv executable")
76+
ap.add_argument("--cwd", default="", help="Working directory for imiv")
77+
ap.add_argument("--backend", default="opengl", help="Runtime backend override")
78+
ap.add_argument(
79+
"--env-script", default=str(default_env_script), help="Optional shell env setup script"
80+
)
81+
ap.add_argument("--out-dir", default=str(default_out_dir), help="Output directory")
82+
ap.add_argument("--open", default=str(default_image), help="Source image to duplicate")
83+
ap.add_argument("--trace", action="store_true", help="Enable runner tracing")
84+
args = ap.parse_args()
85+
86+
exe = Path(args.bin).resolve()
87+
if not exe.exists():
88+
print(f"error: binary not found: {exe}", file=sys.stderr)
89+
return 2
90+
91+
cwd = Path(args.cwd).resolve() if args.cwd else exe.parent.resolve()
92+
out_dir = Path(args.out_dir).resolve()
93+
out_dir.mkdir(parents=True, exist_ok=True)
94+
95+
image_a = Path(args.open).resolve()
96+
if not image_a.exists():
97+
print(f"error: image not found: {image_a}", file=sys.stderr)
98+
return 2
99+
image_b = out_dir / f"{image_a.stem}_copy{image_a.suffix}"
100+
shutil.copyfile(image_a, image_b)
101+
102+
env = _load_env_from_script(Path(args.env_script).resolve())
103+
env["IMIV_CONFIG_HOME"] = str(out_dir / "cfg")
104+
105+
screenshot_path = out_dir / "opengl_multiopen_ocio.png"
106+
layout_path = out_dir / "opengl_multiopen_ocio.layout.json"
107+
state_path = out_dir / "opengl_multiopen_ocio.state.json"
108+
log_path = out_dir / "opengl_multiopen_ocio.log"
109+
110+
cmd = [
111+
sys.executable,
112+
str(runner),
113+
"--bin",
114+
str(exe),
115+
"--cwd",
116+
str(cwd),
117+
"--backend",
118+
args.backend,
119+
"--open",
120+
str(image_a),
121+
"--open",
122+
str(image_b),
123+
"--ocio-use",
124+
"true",
125+
"--screenshot-out",
126+
str(screenshot_path),
127+
"--layout-json-out",
128+
str(layout_path),
129+
"--layout-items",
130+
"--state-json-out",
131+
str(state_path),
132+
]
133+
if args.trace:
134+
cmd.append("--trace")
135+
136+
with log_path.open("w", encoding="utf-8") as log_handle:
137+
proc = subprocess.run(
138+
cmd,
139+
cwd=str(repo_root),
140+
env=env,
141+
check=False,
142+
stdout=log_handle,
143+
stderr=subprocess.STDOUT,
144+
timeout=90,
145+
)
146+
if proc.returncode != 0:
147+
print(f"error: runner exited with code {proc.returncode}", file=sys.stderr)
148+
return 1
149+
150+
for required in (screenshot_path, layout_path, state_path):
151+
if not required.exists():
152+
print(f"error: missing output: {required}", file=sys.stderr)
153+
return 1
154+
155+
log_text = log_path.read_text(encoding="utf-8", errors="ignore")
156+
for pattern in ERROR_PATTERNS:
157+
if pattern in log_text:
158+
print(f"error: found runtime error pattern: {pattern}", file=sys.stderr)
159+
return 1
160+
161+
layout = json.loads(layout_path.read_text(encoding="utf-8"))
162+
if not any(window.get("name") == "Image" for window in layout.get("windows", [])):
163+
print("error: layout dump missing Image window", file=sys.stderr)
164+
return 1
165+
166+
state = json.loads(state_path.read_text(encoding="utf-8"))
167+
if not state.get("image_path"):
168+
print("error: state dump missing image_path", file=sys.stderr)
169+
return 1
170+
if int(state.get("loaded_image_count", 0)) < 2:
171+
print("error: expected at least 2 loaded images in session", file=sys.stderr)
172+
return 1
173+
174+
return 0
175+
176+
177+
if __name__ == "__main__":
178+
raise SystemExit(main())

0 commit comments

Comments
 (0)