File size: 49,950 Bytes
2f382c4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
#!/usr/bin/env python3
"""Render one accessibility geometry mesh into a compact turntable review set.

Open3D is imported only after the render environment has been configured.  This
keeps argument/manifest helpers usable on CPU-only review machines and makes a
software fallback an explicit, auditable user choice.
"""

from __future__ import annotations

import argparse
import json
import math
import os
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Mapping, MutableMapping, Sequence

import imageio.v2 as imageio
import numpy as np
from PIL import Image, ImageDraw, ImageOps


REQUESTED_RENDERER = "Open3D/EGL"
RENDERER_IMPLEMENTATION = "open3d.visualization.rendering.OffscreenRenderer"
PRODUCTION_RENDER_DEVICE = "physical_nvidia_gpu"
DOMINANT_COMPONENT_MIN_FACE_FRACTION = 0.90
GL_VENDOR = 0x1F00
GL_RENDERER = 0x1F01
GL_VERSION = 0x1F02
GL_STRING_NAMES = {
    GL_VENDOR: "GL_VENDOR",
    GL_RENDERER: "GL_RENDERER",
    GL_VERSION: "GL_VERSION",
}
SOFTWARE_OPENGL_MARKERS = (
    "llvmpipe",
    "softpipe",
    "swrast",
    "software",
    "swiftshader",
    "lavapipe",
)
NVIDIA_EGL_VENDOR_JSON = Path(
    "/usr/share/glvnd/egl_vendor.d/10_nvidia.json"
)
OUTPUT_NAMES = {
    "video": "turntable.mp4",
    "gif": "turntable.gif",
    "poster": "turntable_poster.png",
    "multiview": "multiview.jpg",
}


def parse_color(value: str) -> tuple[float, float, float, float]:
    """Parse an RGB/RGBA command-line color in the inclusive [0, 1] range."""
    values = [
        float(item.strip())
        for item in value.replace(":", ",").replace(";", ",").split(",")
        if item.strip()
    ]
    if len(values) == 3:
        values.append(1.0)
    if len(values) != 4:
        raise argparse.ArgumentTypeError("expected three or four comma-separated numbers")
    if any(not math.isfinite(item) or item < 0.0 or item > 1.0 for item in values):
        raise argparse.ArgumentTypeError("background components must be between 0 and 1")
    return tuple(values)  # type: ignore[return-value]


def parse_yaws(value: str) -> tuple[float, ...]:
    """Parse the yaw angles used in the multiview contact sheet."""
    try:
        values = tuple(float(item.strip()) for item in value.split(",") if item.strip())
    except ValueError as error:
        raise argparse.ArgumentTypeError("multiview yaws must be comma-separated numbers") from error
    if not values:
        raise argparse.ArgumentTypeError("at least one multiview yaw is required")
    if len(values) > 12:
        raise argparse.ArgumentTypeError("at most 12 multiview yaws are supported")
    if any(not math.isfinite(item) for item in values):
        raise argparse.ArgumentTypeError("multiview yaws must be finite")
    return values


def build_argument_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--mesh",
        "--ply",
        dest="mesh",
        required=True,
        help="Input triangle mesh. --ply is retained as a convenient alias.",
    )
    parser.add_argument("--output-dir", required=True)
    parser.add_argument("--sample-id", default=None)
    parser.add_argument("--category", default="unknown")
    parser.add_argument("--width", type=int, default=960)
    parser.add_argument("--height", type=int, default=720)
    parser.add_argument("--frames", type=int, default=96)
    parser.add_argument("--fps", type=int, default=24)
    parser.add_argument("--fov", type=float, default=32.0)
    parser.add_argument("--elevation", type=float, default=18.0)
    parser.add_argument("--distance-scale", type=float, default=1.25)
    parser.add_argument("--shader", default="defaultUnlit")
    parser.add_argument(
        "--vertex-colors-srgb",
        action=argparse.BooleanOptionalAction,
        default=True,
        help=(
            "Treat byte RGB vertex colors sampled from photographs as sRGB. "
            "Enabled by default to avoid linear-color whitening."
        ),
    )
    parser.add_argument("--background", type=parse_color, default=parse_color("0.82,0.82,0.80,1"))
    parser.add_argument("--smooth-iterations", type=int, default=0)
    parser.add_argument(
        "--orbit-mode",
        choices=("full_360", "front_arc"),
        default="front_arc",
        help=(
            "full_360 is a conventional object turntable. front_arc keeps an "
            "open-world scene in camera-facing review views and avoids treating "
            "its unobserved back side as reconstructed evidence."
        ),
    )
    parser.add_argument(
        "--orbit-span",
        type=float,
        default=140.0,
        help="Total yaw span in degrees for --orbit-mode front_arc.",
    )
    parser.add_argument(
        "--multiview-yaws",
        type=parse_yaws,
        default=parse_yaws("-60,-30,0,30,60"),
        help=(
            "Comma-separated camera-facing yaw angles. In front_arc mode every "
            "angle must stay within half of --orbit-span."
        ),
    )
    parser.add_argument(
        "--require-vertex-colors",
        action="store_true",
        help=(
            "Reject meshes without a complete finite RGB value for every rendered "
            "vertex instead of silently producing an untextured white surface."
        ),
    )
    parser.add_argument(
        "--cpu-fallback",
        action="store_true",
        # Retained only for low-level renderer diagnostics and unit tests. It is
        # intentionally absent from normal CLI help and every production
        # orchestrator; production rendering is physical-NVIDIA-GPU only.
        help=argparse.SUPPRESS,
    )
    return parser


def validate_arguments(args: argparse.Namespace) -> None:
    if args.width < 64 or args.height < 64:
        raise ValueError("--width and --height must each be at least 64")
    if args.frames < 2:
        raise ValueError("--frames must be at least 2")
    if args.fps < 1:
        raise ValueError("--fps must be at least 1")
    if not math.isfinite(args.fov) or not (5.0 <= args.fov < 170.0):
        raise ValueError("--fov must satisfy 5 <= fov < 170")
    if not math.isfinite(args.elevation):
        raise ValueError("--elevation must be finite")
    if not math.isfinite(args.distance_scale) or args.distance_scale <= 0:
        raise ValueError("--distance-scale must be positive")
    if args.smooth_iterations < 0:
        raise ValueError("--smooth-iterations must be non-negative")
    if not (20.0 <= args.orbit_span <= 240.0):
        raise ValueError("--orbit-span must satisfy 20 <= span <= 240")
    if args.orbit_mode == "front_arc":
        half_span = 0.5 * args.orbit_span
        if any(abs(yaw) > half_span + 1e-6 for yaw in args.multiview_yaws):
            raise ValueError(
                "front_arc multiview yaws must stay within half of --orbit-span"
            )


def _visibility_state(value: str | None) -> str:
    if value is None:
        return "unspecified"
    if not value.strip() or value.strip().lower() in {"-1", "none", "void", "nodevfiles"}:
        return "hidden_or_disabled"
    return "configured_visible_set"


def inspect_render_environment(
    environment: Mapping[str, str] | None = None,
    *,
    cpu_fallback: bool = False,
) -> dict[str, Any]:
    """Return environment evidence without claiming that Open3D used a GPU.

    CUDA visibility and EGL configuration are useful diagnostics, but neither
    proves which physical renderer Filament selected.  The returned status is
    intentionally conservative.
    """
    env = os.environ if environment is None else environment
    cuda_visible = env.get("CUDA_VISIBLE_DEVICES")
    egl_platform = env.get("EGL_PLATFORM")
    software_value = env.get("LIBGL_ALWAYS_SOFTWARE")
    software_requested = cpu_fallback or software_value == "1"
    return {
        "slurm": {
            "job_id": env.get("SLURM_JOB_ID"),
            "job_gpus": env.get("SLURM_JOB_GPUS"),
            "gpu_allocation_present": bool(env.get("SLURM_JOB_ID")),
        },
        "egl": {
            "platform": egl_platform,
            "platform_configured": egl_platform is not None,
            "visibility": "configured" if egl_platform is not None else "unspecified",
            "device_id": env.get("EGL_DEVICE_ID"),
            "gpu_use": "not_proven",
        },
        "cuda": {
            "visible_devices": cuda_visible,
            "visibility": _visibility_state(cuda_visible),
            "used_by_renderer": "not_proven",
        },
        "software_rendering": {
            "requested": software_requested,
            "libgl_always_software": software_value,
            "mesa_loader_driver_override": env.get("MESA_LOADER_DRIVER_OVERRIDE"),
            "gallium_driver": env.get("GALLIUM_DRIVER"),
        },
        "hardware_acceleration": {
            "status": (
                "software_fallback_requested_backend_unverified"
                if software_requested
                else "hardware_backend_unverified"
            ),
            "reason": (
                "Open3D OffscreenRenderer does not expose reliable physical-device "
                "identity through this render path; environment visibility alone is "
                "not hardware proof."
            ),
        },
    }


def require_slurm_gpu_environment(
    environment: Mapping[str, str] | None = None,
) -> dict[str, Any]:
    """Fail closed unless rendering runs inside a visible Slurm GPU allocation."""

    env = os.environ if environment is None else environment
    job_id = str(env.get("SLURM_JOB_ID") or "").strip()
    cuda_visible = str(env.get("CUDA_VISIBLE_DEVICES") or "").strip()
    if not job_id:
        raise RuntimeError(
            "GPU rendering is required by default and must run inside a Slurm "
            "allocation. Use sbatch/srun, or explicitly pass --cpu-fallback "
            "for a diagnostic software render."
        )
    if not cuda_visible or cuda_visible.lower() in {"-1", "none", "void", "nodevfiles"}:
        raise RuntimeError(
            "The Slurm render job has no visible CUDA device; refusing an "
            "implicit CPU/software fallback."
        )
    return {
        "policy": "slurm_gpu_required_unless_cpu_fallback_explicit",
        "slurm_job_id": job_id,
        "cuda_visible_devices": cuda_visible,
        "gpu_allocation_contract_satisfied": True,
    }


def configure_render_environment(
    cpu_fallback: bool,
    environment: MutableMapping[str, str] | None = None,
) -> dict[str, Any]:
    """Configure EGL before importing Open3D and return auditable evidence."""
    env = os.environ if environment is None else environment
    original = {
        key: env.get(key)
        for key in (
            "EGL_PLATFORM",
            "CUDA_VISIBLE_DEVICES",
            "__EGL_VENDOR_LIBRARY_FILENAMES",
            "LIBGL_ALWAYS_SOFTWARE",
            "MESA_LOADER_DRIVER_OVERRIDE",
            "GALLIUM_DRIVER",
        )
    }
    env.setdefault("EGL_PLATFORM", "surfaceless")
    gpu_contract = None
    if cpu_fallback:
        env["LIBGL_ALWAYS_SOFTWARE"] = "1"
        env.setdefault("MESA_LOADER_DRIVER_OVERRIDE", "llvmpipe")
        env.setdefault("GALLIUM_DRIVER", "llvmpipe")
    else:
        gpu_contract = require_slurm_gpu_environment(env)
        if NVIDIA_EGL_VENDOR_JSON.is_file():
            env.setdefault(
                "__EGL_VENDOR_LIBRARY_FILENAMES",
                str(NVIDIA_EGL_VENDOR_JSON),
            )
    evidence = inspect_render_environment(env, cpu_fallback=cpu_fallback)
    evidence["configuration"] = {
        "original": original,
        "egl_platform_defaulted_by_tool": original["EGL_PLATFORM"] is None,
        "nvidia_egl_vendor_defaulted_by_tool": (
            not cpu_fallback
            and original["__EGL_VENDOR_LIBRARY_FILENAMES"] is None
            and env.get("__EGL_VENDOR_LIBRARY_FILENAMES")
            == str(NVIDIA_EGL_VENDOR_JSON)
        ),
        "cpu_fallback_explicit": cpu_fallback,
        "gpu_contract": gpu_contract,
    }
    return evidence


def _load_gl_get_string() -> tuple[Any, str]:
    """Resolve ``glGetString`` without importing an OpenGL Python package.

    Open3D wheels do not expose Filament's selected renderer through their
    Python API.  Once ``OffscreenRenderer`` has created its EGL context, the GL
    dispatch function is sufficient to query the active context directly.
    """

    import ctypes
    import ctypes.util

    attempts: list[str] = []
    candidates: list[tuple[str, str | None]] = [("process", None)]
    for library_name in ("GL", "OpenGL", "GLESv2"):
        library_path = ctypes.util.find_library(library_name)
        if library_path:
            candidates.append((library_name, library_path))

    for label, library_path in candidates:
        try:
            library = ctypes.CDLL(library_path)
            gl_get_string = getattr(library, "glGetString")
        except (AttributeError, OSError) as error:
            attempts.append(f"{label}: {error}")
            continue
        gl_get_string.argtypes = [ctypes.c_uint]
        gl_get_string.restype = ctypes.c_char_p
        return gl_get_string, f"ctypes:{label}"

    # Some EGL/GLES deployments expose GL entry points only through
    # eglGetProcAddress.  Keep this as a final standards-based resolution path.
    egl_path = ctypes.util.find_library("EGL")
    if egl_path:
        try:
            egl = ctypes.CDLL(egl_path)
            egl_get_proc_address = egl.eglGetProcAddress
            egl_get_proc_address.argtypes = [ctypes.c_char_p]
            egl_get_proc_address.restype = ctypes.c_void_p
            address = egl_get_proc_address(b"glGetString")
            if address:
                prototype = ctypes.CFUNCTYPE(ctypes.c_char_p, ctypes.c_uint)
                return prototype(address), "ctypes:EGL.eglGetProcAddress"
            attempts.append("EGL.eglGetProcAddress: returned NULL")
        except (AttributeError, OSError) as error:
            attempts.append(f"EGL: {error}")

    detail = "; ".join(attempts) or "no OpenGL libraries were discoverable"
    raise RuntimeError(f"Could not resolve glGetString ({detail})")


def _decode_gl_string(value: Any) -> str | None:
    """Convert a ``glGetString`` result into a non-empty Python string."""

    if value is None:
        return None
    if isinstance(value, bytes):
        decoded = value.decode("utf-8", errors="replace")
    elif isinstance(value, str):
        decoded = value
    else:
        decoded = str(value)
    decoded = decoded.strip()
    return decoded or None


def query_current_opengl_context(
    gl_get_string: Any | None = None,
    *,
    query_source: str | None = None,
) -> dict[str, Any]:
    """Query the OpenGL strings for the context current on this thread.

    The optional callable is an injection seam for CPU-only unit tests.  In
    production this function is called immediately after constructing Open3D's
    ``OffscreenRenderer``.
    """

    evidence: dict[str, Any] = {
        "query_api": "glGetString",
        "query_source": query_source,
        "query_succeeded": False,
        "GL_VENDOR": None,
        "GL_RENDERER": None,
        "GL_VERSION": None,
        "software_renderer_detected": False,
        "software_markers": [],
        "verification_status": "active_context_unverified",
        "error": None,
    }
    try:
        if gl_get_string is None:
            gl_get_string, resolved_source = _load_gl_get_string()
            evidence["query_source"] = resolved_source
        elif evidence["query_source"] is None:
            evidence["query_source"] = "injected_gl_get_string"

        errors: list[str] = []
        for enum_value, field_name in GL_STRING_NAMES.items():
            try:
                evidence[field_name] = _decode_gl_string(gl_get_string(enum_value))
            except Exception as error:  # ctypes failures must become fail-closed evidence.
                errors.append(f"{field_name}: {type(error).__name__}: {error}")
        missing = [name for name in GL_STRING_NAMES.values() if not evidence[name]]
        if missing:
            errors.append("NULL or empty values: " + ", ".join(missing))
        evidence["query_succeeded"] = not errors
        if errors:
            evidence["error"] = "; ".join(errors)
    except Exception as error:
        evidence["error"] = f"{type(error).__name__}: {error}"

    combined = " ".join(
        str(evidence[name] or "").lower() for name in GL_STRING_NAMES.values()
    )
    markers = [marker for marker in SOFTWARE_OPENGL_MARKERS if marker in combined]
    evidence["software_markers"] = markers
    evidence["software_renderer_detected"] = bool(markers)
    if evidence["query_succeeded"]:
        evidence["verification_status"] = (
            "active_software_context_verified"
            if markers
            else "active_non_software_context_verified"
        )
    return evidence


def inspect_process_graphics_backend(
    *,
    maps_text: str | None = None,
    fd_targets: Sequence[str] | None = None,
    environment: Mapping[str, str] | None = None,
) -> dict[str, Any]:
    """Inspect process-wide EGL libraries and open GPU device handles.

    Filament owns its OpenGL context on an internal render thread, so
    ``glGetString`` from the Python thread may legitimately return NULL.  In
    that case a process-wide NVIDIA EGL binding plus an open physical
    ``/dev/nvidiaN`` handle is stronger evidence than CUDA visibility alone.
    Injection seams keep this policy fully unit-testable without a GPU.
    """

    env = os.environ if environment is None else environment
    errors: list[str] = []
    if maps_text is None:
        try:
            maps_text = Path("/proc/self/maps").read_text(
                encoding="utf-8",
                errors="replace",
            )
        except OSError as error:
            maps_text = ""
            errors.append(f"maps: {type(error).__name__}: {error}")
    if fd_targets is None:
        discovered_targets: list[str] = []
        try:
            for entry in Path("/proc/self/fd").iterdir():
                try:
                    discovered_targets.append(os.readlink(entry))
                except OSError:
                    continue
        except OSError as error:
            errors.append(f"fd: {type(error).__name__}: {error}")
        fd_targets = discovered_targets

    mapped_paths = sorted(
        {
            line.rsplit(None, 1)[-1]
            for line in maps_text.splitlines()
            if line.strip() and "/" in line.rsplit(None, 1)[-1]
        }
    )
    nvidia_egl_libraries = [
        path
        for path in mapped_paths
        if (
            "libegl_nvidia" in path.lower()
            or "libnvidia-eglcore" in path.lower()
        )
    ]
    software_library_markers = sorted(
        {
            marker
            for path in mapped_paths
            for marker in (
                "libegl_mesa",
                "llvmpipe",
                "softpipe",
                "swrast",
                "lavapipe",
                "swiftshader",
            )
            if marker in path.lower()
        }
    )
    gpu_device_fds = sorted(
        {target for target in fd_targets if target.startswith("/dev/nvidia")}
    )
    physical_gpu_fds = [
        target
        for target in gpu_device_fds
        if re.fullmatch(r"/dev/nvidia\d+", target)
    ]
    nvidia_backend_verified = bool(
        nvidia_egl_libraries
        and physical_gpu_fds
        and not software_library_markers
    )
    return {
        "inspection_api": "/proc/self/maps + /proc/self/fd",
        "nvidia_egl_libraries": nvidia_egl_libraries,
        "software_library_markers": software_library_markers,
        "gpu_device_fds": gpu_device_fds,
        "physical_gpu_fds": physical_gpu_fds,
        "forced_egl_vendor_json": env.get(
            "__EGL_VENDOR_LIBRARY_FILENAMES"
        ),
        "nvidia_process_backend_verified": nvidia_backend_verified,
        "verification_status": (
            "nvidia_egl_and_physical_device_fd_verified"
            if nvidia_backend_verified
            else "process_graphics_backend_unverified"
        ),
        "errors": errors,
    }


def enforce_opengl_context_policy(
    opengl_context: Mapping[str, Any],
    *,
    cpu_fallback: bool,
    process_backend: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
    """Apply the fail-closed GPU policy to actual active-context evidence."""

    query_succeeded = bool(opengl_context.get("query_succeeded"))
    software_detected = bool(opengl_context.get("software_renderer_detected"))
    renderer_name = opengl_context.get("GL_RENDERER")
    nvidia_process_verified = bool(
        process_backend
        and process_backend.get("nvidia_process_backend_verified")
    )

    if cpu_fallback:
        if not query_succeeded:
            status = "cpu_fallback_requested_context_unverified"
            reason = (
                "Explicit --cpu-fallback permits diagnostic rendering even though "
                "the active OpenGL strings could not be verified."
            )
        elif software_detected:
            status = "software_fallback_active_context_verified"
            reason = (
                "Explicit --cpu-fallback was requested and the active OpenGL "
                "context identifies a software renderer."
            )
        else:
            status = "cpu_fallback_requested_non_software_context_verified"
            reason = (
                "Explicit --cpu-fallback was requested, but the active OpenGL "
                "strings do not identify a known software renderer."
            )
        return {
            "status": status,
            "reason": reason,
            "active_context_verified": query_succeeded,
            "software_renderer_detected": software_detected,
            "physical_gpu_identity_proven": False,
        }

    if software_detected:
        markers = ", ".join(
            str(value) for value in opengl_context.get("software_markers", [])
        )
        raise RuntimeError(
            "The active OpenGL context is a software OpenGL renderer "
            f"(GL_RENDERER={renderer_name!r}; markers={markers}); refusing the "
            "default GPU render. Use --cpu-fallback to opt in explicitly."
        )
    if nvidia_process_verified:
        physical_gpu_fds = list(
            process_backend.get("physical_gpu_fds", [])
            if process_backend is not None
            else []
        )
        return {
            "status": "active_nvidia_egl_process_backend_verified",
            "reason": (
                (
                    "The active OpenGL context contains no software-renderer "
                    "marker, and the renderer process loaded NVIDIA EGL without "
                    "a known software library and opened a physical NVIDIA GPU "
                    "device."
                )
                if query_succeeded
                else (
                    "Filament's render-thread GL strings were not visible to "
                    "Python, but the renderer process loaded NVIDIA EGL without "
                    "a known software library and opened a physical NVIDIA GPU "
                    "device."
                )
            ),
            "active_context_verified": query_succeeded,
            "process_backend_verified": True,
            "software_renderer_detected": False,
            "physical_gpu_identity_proven": True,
            "physical_gpu_fds": physical_gpu_fds,
        }
    if not query_succeeded:
        detail = str(opengl_context.get("error") or "no GL strings were returned")
        raise RuntimeError(
            "Could not verify the active OpenGL context after creating "
            f"OffscreenRenderer; refusing the default GPU render ({detail}). "
            "Use --cpu-fallback only for an explicit diagnostic software render."
        )
    raise RuntimeError(
        "The active OpenGL strings contain no known software marker, but the "
        "renderer process did not prove an NVIDIA EGL backend plus a physical "
        "/dev/nvidiaN device; refusing the default GPU render. Use "
        "--cpu-fallback only for an explicit diagnostic software render."
    )


def _merge_runtime_graphics_evidence(
    environment_evidence: Mapping[str, Any],
    render_result: Mapping[str, Any],
) -> dict[str, Any]:
    """Combine pre-import environment facts with post-context GL evidence."""

    merged = dict(environment_evidence)
    opengl_context = render_result.get("opengl_context")
    process_backend = render_result.get("process_graphics_backend")
    hardware_acceleration = render_result.get("hardware_acceleration")
    if isinstance(opengl_context, Mapping):
        merged["opengl_context"] = dict(opengl_context)
    if isinstance(process_backend, Mapping):
        merged["process_graphics_backend"] = dict(process_backend)
    if isinstance(hardware_acceleration, Mapping):
        merged["hardware_acceleration"] = dict(hardware_acceleration)
    return merged


def relative_path(path: Path, anchor: Path) -> str:
    """Return a portable relative path, even for an input outside ``anchor``."""
    return Path(os.path.relpath(path.resolve(), anchor.resolve())).as_posix()


def file_record(path: Path, anchor: Path) -> dict[str, Any]:
    import hashlib

    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return {
        "path": relative_path(path, anchor),
        "sha256": digest.hexdigest(),
        "bytes": path.stat().st_size,
    }


def build_render_manifest(
    *,
    mesh_path: Path,
    output_dir: Path,
    sample_id: str,
    category: str,
    render_settings: Mapping[str, Any],
    render_result: Mapping[str, Any],
    environment_evidence: Mapping[str, Any],
    cpu_fallback: bool,
) -> dict[str, Any]:
    """Build a manifest whose filesystem paths are all relative."""
    runtime_evidence = _merge_runtime_graphics_evidence(
        environment_evidence,
        render_result,
    )
    opengl_context = dict(runtime_evidence.get("opengl_context", {}))
    process_backend = dict(
        runtime_evidence.get("process_graphics_backend", {})
    )
    outputs = {
        role: file_record(output_dir / filename, output_dir)
        for role, filename in OUTPUT_NAMES.items()
    }
    return {
        "schema_version": "accessibilityamodal_turntable_v2",
        "created_at_utc": datetime.now(timezone.utc).isoformat(),
        "sample_id": sample_id,
        "category": category,
        "requested_renderer": REQUESTED_RENDERER,
        "renderer": {
            "requested": REQUESTED_RENDERER,
            "implementation": RENDERER_IMPLEMENTATION,
            "production_render_device": PRODUCTION_RENDER_DEVICE,
            "production_gpu_fail_closed": True,
            "requested_execution": (
                "cpu_fallback_explicit"
                if cpu_fallback
                else "slurm_gpu_required_egl"
            ),
            "cpu_fallback_explicit": cpu_fallback,
            "gpu_required_by_default": True,
            "render_call_succeeded": True,
            "hardware_acceleration_status": runtime_evidence[
                "hardware_acceleration"
            ]["status"],
            "opengl_context": opengl_context,
            "process_graphics_backend": process_backend,
        },
        "environment_evidence": runtime_evidence,
        "input": {
            "mesh": file_record(mesh_path, output_dir),
        },
        "outputs": outputs,
        "render_settings": dict(render_settings),
        "mesh_summary": {
            "vertices": int(render_result["vertices"]),
            "renderable_vertices": int(
                render_result.get("renderable_vertices", render_result["vertices"])
            ),
            "unreferenced_vertices_excluded_from_camera_fit": int(
                render_result.get(
                    "unreferenced_vertices_excluded_from_camera_fit",
                    0,
                )
            ),
            "faces": int(render_result["faces"]),
            "camera_framing": dict(render_result.get("camera_framing", {})),
            "camera_motion": dict(render_result.get("camera", {})),
            "vertex_colors": dict(render_result.get("vertex_colors", {})),
        },
        "scene_review": dict(render_result.get("orbit", {})),
        "limitations": [
            "GPU mode rejects known software GL strings; when Filament keeps its context on a render thread, NVIDIA EGL mappings plus a physical /dev/nvidiaN handle provide process-backend proof.",
            "When one connected surface contains at least 90% of faces, camera framing prioritizes it; detached islands remain in the mesh but may fall outside a close-up view.",
            "front_arc shows the observed scene-facing hemisphere and intentionally does not claim an inferred back side.",
            "This rotating geometry is review evidence, not metric accessibility or navigation certification.",
        ],
        "path_policy": "All filesystem paths in this manifest are relative to manifest.json.",
    }


def _camera_pose(
    center: np.ndarray,
    camera_distance: float,
    yaw_degrees: float,
    elevation_degrees: float,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
    yaw = math.radians(yaw_degrees)
    elevation = math.radians(elevation_degrees)
    # ``camera_distance`` already includes the requested framing margin.  Keep
    # that margin in one place so --distance-scale is not applied twice.
    distance = max(camera_distance, 1e-3)
    eye = center + np.array(
        [
            distance * math.sin(yaw) * math.cos(elevation),
            -distance * math.cos(yaw) * math.cos(elevation),
            distance * math.sin(elevation),
        ],
        dtype=np.float64,
    )
    return eye, center, np.array([0.0, 0.0, 1.0], dtype=np.float64)


def _orbit_yaws(
    frames: int,
    *,
    mode: str,
    span_degrees: float,
) -> list[float]:
    """Return deterministic yaw positions for object or open-scene review."""

    if frames < 2:
        raise ValueError("orbit requires at least two frames")
    if mode == "full_360":
        return [360.0 * index / frames for index in range(frames)]
    if mode != "front_arc":
        raise ValueError(f"Unsupported orbit mode: {mode}")
    half_span = 0.5 * float(span_degrees)
    # Begin at the input-facing view, sweep right, return through the front,
    # sweep left, and finish near the front. This loops without a 360-degree
    # discontinuity and never claims an unobserved back-side reconstruction.
    return [
        half_span * math.sin(2.0 * math.pi * index / frames)
        for index in range(frames)
    ]


def _fitted_camera_distance(
    bbox_extent: np.ndarray,
    vertical_fov_degrees: float,
    aspect_ratio: float,
) -> float:
    radius = max(float(np.linalg.norm(bbox_extent)) * 0.5, 1e-3)
    vertical_fov = math.radians(vertical_fov_degrees)
    horizontal_fov = 2.0 * math.atan(math.tan(vertical_fov * 0.5) * aspect_ratio)
    limiting_fov = min(vertical_fov, horizontal_fov)
    return radius / max(math.sin(limiting_fov * 0.5), 1e-3)


def _fitted_camera_distance_for_view(
    vertices: np.ndarray,
    center: np.ndarray,
    *,
    yaw_degrees: float,
    elevation_degrees: float,
    vertical_fov_degrees: float,
    aspect_ratio: float,
    distance_scale: float,
) -> float:
    """Fit an elongated surface for one view without a global-sphere zoom-out."""

    yaw = math.radians(yaw_degrees)
    elevation = math.radians(elevation_degrees)
    eye_direction = np.array(
        [
            math.sin(yaw) * math.cos(elevation),
            -math.cos(yaw) * math.cos(elevation),
            math.sin(elevation),
        ],
        dtype=np.float64,
    )
    forward = -eye_direction
    world_up = np.array([0.0, 0.0, 1.0], dtype=np.float64)
    right = np.cross(forward, world_up)
    right_norm = float(np.linalg.norm(right))
    if right_norm < 1e-8:
        right = np.array([1.0, 0.0, 0.0], dtype=np.float64)
    else:
        right /= right_norm
    view_up = np.cross(right, forward)
    view_up /= max(float(np.linalg.norm(view_up)), 1e-8)

    relative = np.asarray(vertices, dtype=np.float64) - center
    projected_x = np.abs(relative @ right)
    projected_y = np.abs(relative @ view_up)
    toward_camera = relative @ eye_direction
    vertical_half_fov = math.radians(vertical_fov_degrees) * 0.5
    horizontal_half_fov = math.atan(
        math.tan(vertical_half_fov) * aspect_ratio
    )
    required_x = toward_camera + projected_x / max(
        math.tan(horizontal_half_fov),
        1e-6,
    )
    required_y = toward_camera + projected_y / max(
        math.tan(vertical_half_fov),
        1e-6,
    )
    fitted = max(float(np.max(required_x)), float(np.max(required_y)), 1e-3)
    return fitted * distance_scale


def _fixed_camera_distance_for_views(
    vertices: np.ndarray,
    center: np.ndarray,
    *,
    yaw_degrees: Sequence[float],
    elevation_degrees: float,
    vertical_fov_degrees: float,
    aspect_ratio: float,
    distance_scale: float,
) -> tuple[float, dict[str, Any]]:
    """Fit every requested view once and use the largest distance for all frames."""

    unique_yaws = tuple(dict.fromkeys(float(yaw) for yaw in yaw_degrees))
    if not unique_yaws:
        raise ValueError("fixed camera fitting requires at least one yaw")
    if any(not math.isfinite(yaw) for yaw in unique_yaws):
        raise ValueError("fixed camera fitting requires finite yaws")
    required_distances = [
        _fitted_camera_distance_for_view(
            vertices,
            center,
            yaw_degrees=yaw,
            elevation_degrees=elevation_degrees,
            vertical_fov_degrees=vertical_fov_degrees,
            aspect_ratio=aspect_ratio,
            distance_scale=distance_scale,
        )
        for yaw in unique_yaws
    ]
    fixed_distance = max(required_distances)
    return fixed_distance, {
        "distance_policy": "fixed_max_over_orbit_and_multiview",
        "fixed_distance": float(fixed_distance),
        "fit_yaw_count": len(unique_yaws),
        "fit_yaw_min": float(min(unique_yaws)),
        "fit_yaw_max": float(max(unique_yaws)),
        "per_view_required_distance_min": float(min(required_distances)),
        "per_view_required_distance_max": float(max(required_distances)),
    }


def _summarize_vertex_colors(
    vertex_colors: np.ndarray,
    vertex_count: int,
    *,
    required: bool,
) -> dict[str, Any]:
    """Return auditable texture evidence without inferring visual quality."""

    colors = np.asarray(vertex_colors, dtype=np.float64)
    complete = (
        vertex_count > 0
        and colors.ndim == 2
        and colors.shape == (vertex_count, 3)
    )
    finite = bool(complete and np.isfinite(colors).all())
    summary: dict[str, Any] = {
        "required": bool(required),
        "present": bool(complete),
        "finite": finite,
        "colored_vertices": int(colors.shape[0]) if colors.ndim == 2 else 0,
        "coverage_fraction": 1.0 if complete else 0.0,
    }
    if finite:
        summary.update(
            {
                "channel_mean": [float(value) for value in np.mean(colors, axis=0)],
                "channel_std": [float(value) for value in np.std(colors, axis=0)],
                "channel_min": [float(value) for value in np.min(colors, axis=0)],
                "channel_max": [float(value) for value in np.max(colors, axis=0)],
            }
        )
    return summary


def _referenced_vertex_indices(
    triangles: np.ndarray,
    vertex_count: int,
) -> np.ndarray:
    """Return the vertices that can contribute pixels to a triangle render.

    Some reconstruction meshes retain point-cloud vertices that are not
    referenced by any triangle.  Those vertices are invisible, and a single
    far-away orphan must not expand the camera bounds and shrink the actual
    surface to a few pixels.
    """

    faces = np.asarray(triangles, dtype=np.int64)
    if faces.ndim != 2 or faces.shape[1] != 3 or faces.size == 0:
        raise ValueError("camera framing requires at least one triangular face")
    referenced = np.unique(faces.reshape(-1))
    if int(referenced[0]) < 0 or int(referenced[-1]) >= vertex_count:
        raise ValueError("triangle index is outside the mesh vertex array")
    return referenced


def _camera_framing_vertex_indices(
    triangles: np.ndarray,
    triangle_component_labels: np.ndarray,
    *,
    dominant_component_min_face_fraction: float = DOMINANT_COMPONENT_MIN_FACE_FRACTION,
) -> tuple[np.ndarray, dict[str, Any]]:
    """Choose a review framing surface without letting tiny islands zoom out.

    A strongly dominant connected triangle surface represents the primary
    accessibility target in these reconstructions.  Small detached islands can
    still be rendered, but they should not make the target unreadably small.
    When no component owns at least 90% of faces, all triangle vertices remain
    in the camera fit so legitimate multi-part geometry is not guessed away.
    """

    faces = np.asarray(triangles, dtype=np.int64)
    labels = np.asarray(triangle_component_labels, dtype=np.int64).reshape(-1)
    referenced = _referenced_vertex_indices(faces, int(faces.max()) + 1)
    if labels.shape[0] != faces.shape[0] or np.any(labels < 0):
        raise ValueError("triangle component labels must match triangular faces")
    if not (0.0 < dominant_component_min_face_fraction <= 1.0):
        raise ValueError("dominant component face fraction must be in (0, 1]")

    counts = np.bincount(labels)
    dominant_label = int(np.argmax(counts))
    dominant_faces = int(counts[dominant_label])
    face_count = int(faces.shape[0])
    dominant_fraction = dominant_faces / face_count
    use_dominant = dominant_fraction >= dominant_component_min_face_fraction
    if use_dominant:
        framing = np.unique(faces[labels == dominant_label].reshape(-1))
        scope = "dominant_connected_surface"
    else:
        framing = referenced
        scope = "all_connected_surfaces"
    return framing, {
        "scope": scope,
        "framing_faces": dominant_faces if use_dominant else face_count,
        "framing_vertices": int(len(framing)),
        "dominant_component_faces": dominant_faces,
        "dominant_component_face_fraction": float(dominant_fraction),
        "component_count": int(len(counts)),
        "minimum_dominant_face_fraction": float(
            dominant_component_min_face_fraction
        ),
    }


def _contact_sheet(images: Sequence[np.ndarray], labels: Sequence[str], path: Path) -> None:
    columns = 2 if len(images) > 1 else 1
    rows = math.ceil(len(images) / columns)
    panel_width = 480
    panel_height = 390
    label_height = 34
    canvas = Image.new(
        "RGB",
        (panel_width * columns, panel_height * rows),
        (235, 235, 232),
    )
    for index, (array, label) in enumerate(zip(images, labels)):
        image = Image.fromarray(array).convert("RGB")
        body = ImageOps.contain(image, (panel_width, panel_height - label_height))
        panel = Image.new("RGB", (panel_width, panel_height), "white")
        draw = ImageDraw.Draw(panel)
        draw.rectangle((0, 0, panel_width, label_height), fill=(242, 242, 239))
        draw.text((12, 11), label, fill=(20, 20, 20))
        panel.paste(
            body,
            (
                (panel_width - body.width) // 2,
                label_height + (panel_height - label_height - body.height) // 2,
            ),
        )
        canvas.paste(panel, ((index % columns) * panel_width, (index // columns) * panel_height))
    canvas.save(path, quality=94, subsampling=0)


def render_outputs(
    mesh_path: Path,
    output_dir: Path,
    *,
    width: int,
    height: int,
    frames: int,
    fps: int,
    fov: float,
    elevation: float,
    distance_scale: float,
    shader: str,
    background: Sequence[float],
    smooth_iterations: int,
    multiview_yaws: Sequence[float],
    orbit_mode: str = "front_arc",
    orbit_span: float = 140.0,
    require_vertex_colors: bool = False,
    vertex_colors_srgb: bool = True,
    cpu_fallback: bool = False,
) -> dict[str, Any]:
    """Render all outputs through Open3D's OffscreenRenderer path."""
    import open3d as o3d
    from open3d.visualization import rendering

    mesh = o3d.io.read_triangle_mesh(str(mesh_path))
    if len(mesh.vertices) == 0 or len(mesh.triangles) == 0:
        raise ValueError(f"Input is not a non-empty triangle mesh: {mesh_path}")

    source_vertex_count = len(mesh.vertices)
    referenced_indices = _referenced_vertex_indices(
        np.asarray(mesh.triangles, dtype=np.int64),
        source_vertex_count,
    )
    unreferenced_vertex_count = source_vertex_count - len(referenced_indices)
    # Open3D's scene bounds can include vertices that have no incident face,
    # even though rasterization cannot display them.  Remove only these
    # topologically inert points before both rendering and camera fitting.
    if unreferenced_vertex_count:
        mesh.remove_unreferenced_vertices()

    # Geometry produced from image-camera coordinates is easier to inspect after
    # mapping x/right, z/up, and -y/depth into a conventional scene frame.
    vertices = np.asarray(mesh.vertices, dtype=np.float64)
    mesh.vertices = o3d.utility.Vector3dVector(
        np.stack([vertices[:, 0], vertices[:, 2], -vertices[:, 1]], axis=1)
    )
    if smooth_iterations:
        mesh = mesh.filter_smooth_laplacian(number_of_iterations=smooth_iterations)
    mesh.compute_vertex_normals()
    vertex_color_summary = _summarize_vertex_colors(
        np.asarray(mesh.vertex_colors, dtype=np.float64),
        len(mesh.vertices),
        required=require_vertex_colors,
    )
    if require_vertex_colors and not (
        vertex_color_summary["present"] and vertex_color_summary["finite"]
    ):
        raise ValueError(
            "Input mesh does not provide complete finite vertex colors required "
            f"for texture-preserving rendering: {mesh_path}"
        )

    transformed_vertices = np.asarray(mesh.vertices, dtype=np.float64)
    triangle_component_labels, _, _ = mesh.cluster_connected_triangles()
    framing_indices, framing_summary = _camera_framing_vertex_indices(
        np.asarray(mesh.triangles, dtype=np.int64),
        np.asarray(triangle_component_labels, dtype=np.int64),
    )
    framing_vertices = transformed_vertices[framing_indices]
    framing_min = np.min(framing_vertices, axis=0)
    framing_max = np.max(framing_vertices, axis=0)
    center = (framing_min + framing_max) * 0.5
    orbit_yaws = _orbit_yaws(
        frames,
        mode=orbit_mode,
        span_degrees=orbit_span,
    )
    fixed_camera_distance, camera_summary = _fixed_camera_distance_for_views(
        framing_vertices,
        center,
        yaw_degrees=tuple(orbit_yaws) + tuple(multiview_yaws),
        elevation_degrees=elevation,
        vertical_fov_degrees=fov,
        aspect_ratio=width / height,
        distance_scale=distance_scale,
    )

    output_dir.mkdir(parents=True, exist_ok=True)
    renderer = rendering.OffscreenRenderer(width, height)
    try:
        opengl_context = query_current_opengl_context()
        process_graphics_backend = inspect_process_graphics_backend()
        hardware_acceleration = enforce_opengl_context_policy(
            opengl_context,
            cpu_fallback=cpu_fallback,
            process_backend=process_graphics_backend,
        )
        renderer.scene.set_background(list(background))
        renderer.scene.scene.set_sun_light(
            [0.35, -0.5, -0.75],
            [1.0, 0.97, 0.92],
            65000,
        )
        renderer.scene.scene.enable_sun_light(True)
        renderer.scene.show_axes(False)
        material = rendering.MaterialRecord()
        material.shader = shader
        material.sRGB_color = bool(vertex_colors_srgb)
        material.base_color = [1.0, 1.0, 1.0, 1.0]
        material.base_roughness = 0.68
        material.base_reflectance = 0.18
        renderer.scene.add_geometry("accessibility_geometry", mesh, material)

        def render_yaw(yaw: float) -> np.ndarray:
            eye, look_at, up = _camera_pose(
                center,
                fixed_camera_distance,
                yaw,
                elevation,
            )
            renderer.setup_camera(fov, look_at, eye, up)
            array = np.asarray(renderer.render_to_image())
            if array.ndim != 3 or array.shape[2] not in (3, 4):
                raise RuntimeError(f"Unexpected Open3D render shape: {array.shape}")
            return array[..., :3].astype(np.uint8, copy=False)

        video_frames = [render_yaw(yaw) for yaw in orbit_yaws]
        imageio.mimsave(
            output_dir / OUTPUT_NAMES["video"],
            video_frames,
            fps=fps,
            quality=8,
            macro_block_size=1,
        )
        imageio.mimsave(
            output_dir / OUTPUT_NAMES["gif"],
            video_frames,
            duration=1000.0 / fps,
            loop=0,
        )
        Image.fromarray(video_frames[0]).save(output_dir / OUTPUT_NAMES["poster"])

        views = [render_yaw(yaw) for yaw in multiview_yaws]
        labels = [f"Geometry view {yaw:g} deg" for yaw in multiview_yaws]
        _contact_sheet(views, labels, output_dir / OUTPUT_NAMES["multiview"])
    finally:
        release = getattr(renderer, "release_resources", None)
        if callable(release):
            release()

    return {
        "vertices": source_vertex_count,
        "renderable_vertices": len(mesh.vertices),
        "unreferenced_vertices_excluded_from_camera_fit": unreferenced_vertex_count,
        "faces": len(mesh.triangles),
        "camera_framing": framing_summary,
        "camera": camera_summary,
        "vertex_colors": vertex_color_summary,
        "opengl_context": opengl_context,
        "process_graphics_backend": process_graphics_backend,
        "hardware_acceleration": hardware_acceleration,
        "orbit": {
            "mode": orbit_mode,
            "span_degrees": float(orbit_span),
            "yaw_min": float(min(orbit_yaws)),
            "yaw_max": float(max(orbit_yaws)),
            "multiview_yaws": [float(yaw) for yaw in multiview_yaws],
            "open_world_scene_review": orbit_mode == "front_arc",
        },
    }


def write_json(path: Path, payload: Mapping[str, Any]) -> None:
    path.write_text(
        json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
        encoding="utf-8",
    )


def main(argv: Sequence[str] | None = None) -> int:
    parser = build_argument_parser()
    args = parser.parse_args(argv)
    validate_arguments(args)

    mesh_path = Path(args.mesh).expanduser().resolve()
    if not mesh_path.is_file():
        raise FileNotFoundError(mesh_path)
    output_dir = Path(args.output_dir).expanduser().resolve()
    output_dir.mkdir(parents=True, exist_ok=True)
    sample_id = str(args.sample_id or mesh_path.stem)
    category = str(args.category).strip().lower() or "unknown"

    environment_evidence = configure_render_environment(args.cpu_fallback)
    render_settings = {
        "resolution": [args.width, args.height],
        "frames": args.frames,
        "fps": args.fps,
        "fov_degrees": args.fov,
        "elevation_degrees": args.elevation,
        "distance_scale": args.distance_scale,
        "camera_fit": "fixed_max_projected_primary_surface_v4",
        "camera_distance_policy": "fixed_max_over_orbit_and_multiview",
        "shader": args.shader,
        "background": list(args.background),
        "smooth_iterations": args.smooth_iterations,
        "require_vertex_colors": args.require_vertex_colors,
        "vertex_colors_srgb": args.vertex_colors_srgb,
        "multiview_yaws_degrees": list(args.multiview_yaws),
        "orbit_mode": args.orbit_mode,
        "orbit_span_degrees": args.orbit_span,
        "scene_interpretation": (
            "open_world_accessibility_surface_review"
            if args.orbit_mode == "front_arc"
            else "full_object_turntable"
        ),
    }
    result = render_outputs(
        mesh_path,
        output_dir,
        width=args.width,
        height=args.height,
        frames=args.frames,
        fps=args.fps,
        fov=args.fov,
        elevation=args.elevation,
        distance_scale=args.distance_scale,
        shader=args.shader,
        background=args.background,
        smooth_iterations=args.smooth_iterations,
        multiview_yaws=args.multiview_yaws,
        orbit_mode=args.orbit_mode,
        orbit_span=args.orbit_span,
        require_vertex_colors=args.require_vertex_colors,
        vertex_colors_srgb=args.vertex_colors_srgb,
        cpu_fallback=args.cpu_fallback,
    )
    manifest = build_render_manifest(
        mesh_path=mesh_path,
        output_dir=output_dir,
        sample_id=sample_id,
        category=category,
        render_settings=render_settings,
        render_result=result,
        environment_evidence=environment_evidence,
        cpu_fallback=args.cpu_fallback,
    )
    write_json(output_dir / "manifest.json", manifest)
    print(
        json.dumps(
            {
                "status": "rendered",
                "sample_id": sample_id,
                "output_dir": str(output_dir),
                "cpu_fallback_explicit": args.cpu_fallback,
                "hardware_acceleration_status": manifest["renderer"][
                    "hardware_acceleration_status"
                ],
            },
            ensure_ascii=False,
            sort_keys=True,
        )
    )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())