@genex-ai/cli-demo 1.30.0-dev.645 → 1.31.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.
Files changed (47) hide show
  1. package/README.md +0 -18
  2. package/dist/index.js +2839 -4768
  3. package/package.json +3 -3
  4. package/templates/controllers/character/follow-camera.ts +1 -16
  5. package/templates/controllers/character/meshy/meshy-loader.ts +2 -3
  6. package/templates/controllers/quality/pick-asset.ts +16 -49
  7. package/templates/controllers/shared/physics-world.ts +4 -6
  8. package/templates/skills/genex-ai-character/SKILL.md +15 -77
  9. package/templates/skills/genex-ai-menu/SKILL.md +11 -15
  10. package/templates/skills/genex-ai-model/SKILL.md +7 -45
  11. package/templates/skills/genex-ai-texture/SKILL.md +1 -1
  12. package/templates/skills/genex-ai-video/SKILL.md +17 -75
  13. package/templates/skills/genex-game-director/SKILL.md +46 -112
  14. package/templates/skills/genex-game-director/references/design-contract.md +5 -13
  15. package/templates/skills/genex-game-director/references/routing-map.md +45 -30
  16. package/templates/skills/genex-getting-started/SKILL.md +2 -2
  17. package/templates/skills/genex-monetization/SKILL.md +171 -0
  18. package/templates/skills/genex-threejs-adaptive-quality/SKILL.md +0 -21
  19. package/templates/skills/genex-threejs-character-controller/SKILL.md +5 -17
  20. package/templates/skills/genex-threejs-creatures/SKILL.md +1 -8
  21. package/templates/skills/genex-threejs-embed-auth/SKILL.md +52 -8
  22. package/templates/skills/genex-threejs-game-ui/SKILL.md +6 -55
  23. package/templates/skills/genex-threejs-procedural-assets/SKILL.md +10 -17
  24. package/templates/skills/genex-threejs-visual-validation/SKILL.md +2 -12
  25. package/templates/skills/genex-tool-audio/SKILL.md +2 -3
  26. package/templates/skills/genex-tool-character/SKILL.md +5 -36
  27. package/templates/skills/genex-tool-image/SKILL.md +2 -4
  28. package/templates/skills/genex-tool-model/SKILL.md +6 -32
  29. package/templates/skills/genex-tool-texture/SKILL.md +1 -1
  30. package/templates/skills/genex-tool-video/SKILL.md +5 -28
  31. package/templates/skills/genex-tool-workflow/SKILL.md +1 -4
  32. package/templates/skills/genex-updates/SKILL.md +1 -1
  33. package/dist/blender-mcp-Q6PSFYSE.js +0 -241
  34. package/dist/blender-serve-BF4FZ55Z.js +0 -244
  35. package/dist/chunk-2COG4P3T.js +0 -968
  36. package/dist/chunk-HYCSNWYX.js +0 -126
  37. package/templates/blender-service/demo/castle.py +0 -117
  38. package/templates/blender-service/gpu_witness.py +0 -245
  39. package/templates/blender-service/ops.py +0 -225
  40. package/templates/blender-service/pool.py +0 -910
  41. package/templates/blender-service/server.py +0 -611
  42. package/templates/blender-service/supervisor.py +0 -221
  43. package/templates/blender-service/views.py +0 -281
  44. package/templates/controllers/quality/deadline.ts +0 -117
  45. package/templates/skills/genex-blender-scene/SKILL.md +0 -243
  46. package/templates/skills/genex-lane-card/SKILL.md +0 -78
  47. package/templates/skills/genex-tool-publish/SKILL.md +0 -100
@@ -1,126 +0,0 @@
1
- // src/config.ts
2
- import fs from "fs";
3
- import os from "os";
4
- import path from "path";
5
- import { fileURLToPath } from "url";
6
- var RAW_CHANNEL = "dev";
7
- var CLI_CHANNEL = RAW_CHANNEL === "dev" ? "dev" : "latest";
8
- var STANDS = {
9
- prod: { api: "https://api.genex.games", dashboard: "https://genex.games" },
10
- dev: { api: "https://api-dev.genex.games", dashboard: "https://dev.genex.games" }
11
- };
12
- var DEFAULT_AUTH_URL = CLI_CHANNEL === "dev" ? STANDS.dev.dashboard : STANDS.prod.dashboard;
13
- var DEFAULT_API_URL = CLI_CHANNEL === "dev" ? STANDS.dev.api : STANDS.prod.api;
14
- var DEFAULT_ANIMS_BASE = "https://cdn.genex.technology/anims/ual1/v1/";
15
- var ANIMS_BASE_ENV = "GENEX_ANIMS_BASE";
16
- function getAnimsBase(override) {
17
- const raw = override || process.env[ANIMS_BASE_ENV] || DEFAULT_ANIMS_BASE;
18
- return raw.replace(/\/+$/, "") + "/";
19
- }
20
- function getAnimsCacheDir() {
21
- return path.join(getGenexDir(), "cache", "anims");
22
- }
23
- var ENV_TOKEN_KEY = "GENEX_TOKEN";
24
- var AUTH_URL_ENV = "GENEX_AUTH_URL";
25
- var API_URL_ENV = "GENEX_API_URL";
26
- var ENV_FILE_ENV = "GENEX_ENV_FILE";
27
- function getAuthUrl(override) {
28
- const raw = override || process.env[AUTH_URL_ENV] || DEFAULT_AUTH_URL;
29
- return raw.replace(/\/+$/, "");
30
- }
31
- function getApiUrl(override) {
32
- const raw = override || process.env[API_URL_ENV] || DEFAULT_API_URL;
33
- return raw.replace(/\/+$/, "");
34
- }
35
- function getGenexDir() {
36
- return path.join(os.homedir(), ".genex");
37
- }
38
- function getGenexEnvPath(override) {
39
- if (override) return path.resolve(override);
40
- const fromEnv = process.env[ENV_FILE_ENV];
41
- if (fromEnv) return path.resolve(fromEnv);
42
- return path.join(getGenexDir(), "env");
43
- }
44
- function getTemplatesDir() {
45
- const here = path.dirname(fileURLToPath(import.meta.url));
46
- return path.resolve(here, "..", "templates");
47
- }
48
- function getCliVersion() {
49
- try {
50
- const here = path.dirname(fileURLToPath(import.meta.url));
51
- const pkg = JSON.parse(
52
- fs.readFileSync(path.resolve(here, "..", "package.json"), "utf8")
53
- );
54
- return pkg.version ?? "0.0.0";
55
- } catch {
56
- return "0.0.0";
57
- }
58
- }
59
- var KNOWN_AGENTS = {
60
- claude: { label: "Claude Code", dirName: ".claude", full: true },
61
- codex: { label: "Codex", dirName: ".codex", full: false },
62
- cursor: { label: "Cursor", dirName: ".cursor", full: false }
63
- };
64
- var KNOWN_AGENT_IDS = Object.keys(KNOWN_AGENTS);
65
- function resolveAgentTargets(opts = {}) {
66
- if (opts.dir) {
67
- return [{ id: "custom", label: "workspace", baseDir: path.resolve(opts.dir), full: true }];
68
- }
69
- const home = os.homedir();
70
- const projectRoot = process.cwd();
71
- let ids;
72
- if (opts.agents && opts.agents.length > 0) {
73
- ids = opts.agents.filter((id) => KNOWN_AGENTS[id]);
74
- } else {
75
- ids = KNOWN_AGENT_IDS.filter((id) => {
76
- const dirName = KNOWN_AGENTS[id].dirName;
77
- return isDir(path.join(home, dirName)) || isDir(path.join(projectRoot, dirName));
78
- });
79
- if (ids.length === 0) ids = ["claude"];
80
- }
81
- return ids.map((id) => {
82
- const def = KNOWN_AGENTS[id];
83
- return { id, label: def.label, baseDir: path.join(projectRoot, def.dirName), full: def.full };
84
- });
85
- }
86
- function isDir(p) {
87
- try {
88
- return fs.statSync(p).isDirectory();
89
- } catch {
90
- return false;
91
- }
92
- }
93
-
94
- // src/utils/colors.ts
95
- var useColor = Boolean(process.stdout.isTTY) && process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb";
96
- var ESC = String.fromCharCode(27);
97
- var code = (open, close) => (s) => useColor ? `${ESC}[${open}m${s}${ESC}[${close}m` : s;
98
- var c = {
99
- bold: code(1, 22),
100
- dim: code(2, 22),
101
- red: code(31, 39),
102
- green: code(32, 39),
103
- yellow: code(33, 39),
104
- blue: code(34, 39),
105
- cyan: code(36, 39),
106
- gray: code(90, 39)
107
- };
108
-
109
- export {
110
- CLI_CHANNEL,
111
- STANDS,
112
- DEFAULT_AUTH_URL,
113
- DEFAULT_API_URL,
114
- getAnimsBase,
115
- getAnimsCacheDir,
116
- ENV_TOKEN_KEY,
117
- ENV_FILE_ENV,
118
- getAuthUrl,
119
- getApiUrl,
120
- getGenexDir,
121
- getGenexEnvPath,
122
- getTemplatesDir,
123
- getCliVersion,
124
- resolveAgentTargets,
125
- c
126
- };
@@ -1,117 +0,0 @@
1
- """The M0/M1 acceptance artifact: a castle room built from primitives.
2
-
3
- Deliberately NOT a torture test of Blender -- it is a torture test of the LOOP.
4
- It exercises the things a real composition step needs (booleans, arrays,
5
- materials, instancing, transforms) and produces something a human can judge in
6
- one glance: either the contact sheet shows a castle room, or it does not.
7
- """
8
-
9
- import math
10
-
11
- import bpy
12
-
13
- ROOM = 12.0
14
- WALL_H = 5.0
15
- WALL_T = 0.6
16
-
17
-
18
- def _clear():
19
- bpy.ops.wm.read_factory_settings(use_empty=True)
20
-
21
-
22
- def _mat(name, rgb, rough=0.85):
23
- m = bpy.data.materials.new(name)
24
- m.use_nodes = True
25
- bsdf = m.node_tree.nodes["Principled BSDF"]
26
- bsdf.inputs["Base Color"].default_value = (*rgb, 1.0)
27
- # 'Roughness' is stable across 4.x; 'Specular' was RENAMED in 4.0 and is the
28
- # single most common KeyError in LLM-written bpy. Do not reach for it.
29
- bsdf.inputs["Roughness"].default_value = rough
30
- # BOTH, and the second one is the trap: Workbench's MATERIAL colour mode --
31
- # what every contact sheet renders -- reads `diffuse_color`, NOT the
32
- # Principled BSDF. Setting only the BSDF gives a scene that exports with
33
- # correct colours, reports materialCount>0, and renders ENTIRELY GREY.
34
- # Numbers cannot catch this; the picture can. Measured 2026-08-31.
35
- m.diffuse_color = (*rgb, 1.0)
36
- return m
37
-
38
-
39
- def _cube(name, loc, scale, mat):
40
- bpy.ops.mesh.primitive_cube_add(size=2.0, location=loc)
41
- o = bpy.context.active_object
42
- o.name = name
43
- o.scale = scale
44
- o.data.materials.append(mat)
45
- return o
46
-
47
-
48
- def build():
49
- _clear()
50
- stone = _mat("Stone", (0.46, 0.44, 0.41))
51
- dark = _mat("StoneDark", (0.26, 0.25, 0.24))
52
- wood = _mat("Wood", (0.24, 0.14, 0.07))
53
- roofm = _mat("Roof", (0.35, 0.12, 0.12), rough=0.6)
54
- fire = _mat("Fire", (1.0, 0.45, 0.1), rough=0.4)
55
-
56
- _cube("Floor", (0, 0, -0.25), (ROOM / 2, ROOM / 2, 0.25), dark)
57
-
58
- half = ROOM / 2
59
- # Three solid walls; the south wall is split to leave a doorway.
60
- _cube("Wall_N", (0, half, WALL_H / 2), (half, WALL_T / 2, WALL_H / 2), stone)
61
- _cube("Wall_E", (half, 0, WALL_H / 2), (WALL_T / 2, half, WALL_H / 2), stone)
62
- _cube("Wall_W", (-half, 0, WALL_H / 2), (WALL_T / 2, half, WALL_H / 2), stone)
63
- door = 2.2
64
- side = (ROOM - door) / 4
65
- for sgn in (-1, 1):
66
- _cube(f"Wall_S{'L' if sgn < 0 else 'R'}",
67
- (sgn * (door / 2 + side), -half, WALL_H / 2),
68
- (side, WALL_T / 2, WALL_H / 2), stone)
69
- _cube("Lintel", (0, -half, WALL_H - 0.5), (door / 2, WALL_T / 2, 0.5), stone)
70
-
71
- # Crenellations: a real array, not four hand-placed boxes.
72
- for i in range(9):
73
- x = -half + 0.6 + i * (ROOM - 1.2) / 8
74
- for y in (half, -half):
75
- _cube(f"Cren_{i}_{'n' if y > 0 else 's'}",
76
- (x, y, WALL_H + 0.35), (0.35, WALL_T / 2, 0.35), stone)
77
-
78
- # Corner towers with conical roofs.
79
- for i, (sx, sy) in enumerate(((1, 1), (1, -1), (-1, 1), (-1, -1))):
80
- cx, cy = sx * half, sy * half
81
- bpy.ops.mesh.primitive_cylinder_add(radius=1.5, depth=WALL_H + 2.0,
82
- location=(cx, cy, (WALL_H + 2.0) / 2), vertices=16)
83
- t = bpy.context.active_object
84
- t.name = f"Tower_{i}"
85
- t.data.materials.append(stone)
86
- bpy.ops.mesh.primitive_cone_add(radius1=2.0, depth=2.4, vertices=16,
87
- location=(cx, cy, WALL_H + 2.0 + 1.2))
88
- r = bpy.context.active_object
89
- r.name = f"TowerRoof_{i}"
90
- r.data.materials.append(roofm)
91
-
92
- # A table, so the room reads as inhabited and the sheet has a scale cue.
93
- _cube("TableTop", (0, 1.0, 1.1), (2.2, 0.9, 0.12), wood)
94
- for dx in (-1.9, 1.9):
95
- for dy in (-0.65, 0.65):
96
- _cube(f"Leg_{dx}_{dy}", (dx, 1.0 + dy, 0.55), (0.12, 0.12, 0.55), wood)
97
-
98
- # Wall torches -- emissive-ish spheres; they also prove material assignment.
99
- for i, (tx, ty) in enumerate(((-half + 0.6, 2.5), (-half + 0.6, -2.5),
100
- (half - 0.6, 2.5), (half - 0.6, -2.5))):
101
- _cube(f"Sconce_{i}", (tx, ty, 3.0), (0.15, 0.15, 0.4), wood)
102
- bpy.ops.mesh.primitive_uv_sphere_add(radius=0.32, location=(tx, ty, 3.5), segments=12, ring_count=8)
103
- f = bpy.context.active_object
104
- f.name = f"Flame_{i}"
105
- f.data.materials.append(fire)
106
-
107
- bpy.ops.object.select_all(action="DESELECT")
108
- n = len(bpy.context.scene.objects)
109
- tris = sum(
110
- sum(max(len(p.vertices) - 2, 0) for p in o.data.polygons)
111
- for o in bpy.context.scene.objects if o.type == "MESH"
112
- )
113
- print(f"castle: {n} objects, {tris} tris")
114
- return n, tris
115
-
116
-
117
- build()
@@ -1,245 +0,0 @@
1
- """Fail-closed GPU witness.
2
-
3
- NAMED gpu_witness, NOT gpu: Blender ships a built-in `gpu` module, and a local
4
- `gpu.py` is shadowed by it. Measured 2026-09-01 -- `import gpu` returned
5
- Blender's module, `assert_gpu_or_die` did not exist on it, and the assertion
6
- never ran. The container still failed closed, which hid the mistake.
7
-
8
- There are FIVE independent ways to end up rendering on the CPU with a
9
- correct-looking image, a 200 response and a zero exit code. This module exists
10
- because any one of them alone would let the GPU tier pass every functional test
11
- while delivering nothing but a bill:
12
-
13
- 1. EEVEE RENDERS ON THE CPU. Measured on the CPU image (Mesa 22.3.6): a full
14
- EEVEE contact sheet with correct shadows, 47.5s vs 0.4s for Workbench. So
15
- "EEVEE worked" is a PERFORMANCE signal, never a GPU signal. Dockerfile.gpu
16
- installs no Mesa vendor at all, so there is nothing to fall back onto --
17
- that absence is the backstop, not this module.
18
- 2. `prefs.devices` is EMPTY until refresh_devices() populates it, so the
19
- obvious enabling loop iterates zero elements and reports success.
20
- 3. `scene.cycles.device` defaults to 'CPU' and is stored PER SCENE inside the
21
- .blend, so perfect preferences still render on CPU.
22
- 4. Blanket `d.use = True` enables HYBRID CPU+GPU, which every other check
23
- still calls "GPU".
24
- 5. An uncaught exception under --python exits 0. (Handled by
25
- --python-exit-code 1 in the CMD; nothing here raises, on purpose.)
26
-
27
- Nothing in this module raises. Every failure path calls sys.exit() explicitly.
28
- """
29
-
30
- import os
31
- import sys
32
- import tempfile
33
-
34
- import bpy
35
-
36
- # 4.2/4.5 renamed the engine; it is BLENDER_EEVEE again on 5.0+. One constant.
37
- def _eevee_engine():
38
- """The EEVEE id for THIS Blender. Renamed to BLENDER_EEVEE_NEXT in 4.2 and
39
- renamed BACK to BLENDER_EEVEE in 5.0, so a literal is wrong on one side of
40
- that line — and setting an unknown engine id fails at render, far from the
41
- cause. Resolved from the running build rather than assumed.
42
-
43
- Mirrored by eeveeEngineFor() in apps/cli/src/lib/blender-local.ts, which
44
- gates which Blenders the local lane will drive."""
45
- try:
46
- items = bpy.types.RenderSettings.bl_rna.properties["engine"].enum_items
47
- names = {i.identifier for i in items}
48
- except Exception: # noqa: BLE001
49
- names = set()
50
- for candidate in ("BLENDER_EEVEE_NEXT", "BLENDER_EEVEE"):
51
- if candidate in names:
52
- return candidate
53
- return "BLENDER_EEVEE_NEXT" if bpy.app.version < (5, 0, 0) else "BLENDER_EEVEE"
54
-
55
-
56
- EEVEE_ENGINE = _eevee_engine()
57
-
58
- # Preference order, not a hardcoded value. OptiX first because production is
59
- # NVIDIA, but the vendor is DISCOVERED from what the build actually offers --
60
- # which is what lets the identical service be exercised on an Apple-silicon Mac
61
- # (METAL) or an AMD box (HIP) instead of only on the target hardware.
62
- # GENEX_BLENDER_CYCLES_BACKEND pins one explicitly.
63
- CYCLES_BACKEND_PREFERENCE = ("OPTIX", "CUDA", "METAL", "HIP", "ONEAPI")
64
- SOFTWARE_DEVICES = ("SOFTWARE", "NONE", "UNKNOWN")
65
-
66
-
67
- def _force_context():
68
- """Render 4x4 with EEVEE. Returns True if it actually produced pixels.
69
-
70
- ITS SUCCESS IS NOT EVIDENCE OF A GPU. MEASURED 2026-09-01 on the CPU image
71
- (Debian bookworm, Mesa 22.3.6 llvmpipe): EEVEE renders correctly, with real
72
- shadows, entirely on the CPU -- 47.5s for a 4-view sheet against 0.4s for
73
- Workbench. An earlier version of this module treated a successful probe as
74
- the strongest GPU signal, which would have PASSED on any GPU-less host that
75
- has Mesa installed.
76
-
77
- What it is actually for: forcing the lazily-created GPU context so
78
- gpu.platform can be queried at all, and catching a broken graphics stack.
79
- The real GPU discriminator is configure_cycles() -- enumerating an OptiX
80
- device needs a real driver and cannot be faked by a software rasterizer."""
81
- s = bpy.context.scene
82
- r = s.render
83
- ok = False
84
- keep = (r.engine, r.resolution_x, r.resolution_y, r.filepath,
85
- r.image_settings.file_format, r.resolution_percentage)
86
- try:
87
- r.engine = EEVEE_ENGINE
88
- r.resolution_x = r.resolution_y = 4
89
- r.resolution_percentage = 100
90
- r.image_settings.file_format = "PNG"
91
- # mkstemp, NOT a fixed path. This is a LIVE SINGLE-TENANT BUG, not
92
- # only a multi-seat one: a leftover file from any earlier run makes the
93
- # `getsize() > 0` check below true for a probe that rendered nothing, so
94
- # a GPU-less box passes an assertion whose entire job is to fail there.
95
- # With seats it is also a boot race between children.
96
- fd, probe_path = tempfile.mkstemp(prefix="genex-gpu-probe-", suffix=".png")
97
- os.close(fd)
98
- os.unlink(probe_path) # Blender must create it, or it proves nothing.
99
- r.filepath = probe_path
100
- # A camera is required and a fresh scene has none, so the probe must
101
- # bring its own or it fails for a reason that has nothing to do with
102
- # the GPU. Measured: "Cannot render, no camera".
103
- cam_data = bpy.data.cameras.new("__genex_probe_cam")
104
- cam = bpy.data.objects.new("__genex_probe_cam", cam_data)
105
- s.collection.objects.link(cam)
106
- prev_cam, s.camera = s.camera, cam
107
- try:
108
- bpy.ops.render.render(write_still=True)
109
- ok = os.path.exists(probe_path) and os.path.getsize(probe_path) > 0
110
- finally:
111
- s.camera = prev_cam
112
- bpy.data.objects.remove(cam, do_unlink=True)
113
- bpy.data.cameras.remove(cam_data)
114
- try:
115
- os.unlink(probe_path)
116
- except OSError:
117
- pass
118
- except Exception as e: # noqa: BLE001 - a probe failure is a verdict, not a crash
119
- sys.stderr.write("[gpu] probe render failed: %s\n" % e)
120
- finally:
121
- (r.engine, r.resolution_x, r.resolution_y, r.filepath,
122
- r.image_settings.file_format, r.resolution_percentage) = keep
123
- return ok
124
-
125
-
126
- def configure_cycles():
127
- """Enable OptiX devices and return their names. [] means no GPU device."""
128
- addon = bpy.context.preferences.addons.get("cycles")
129
- if addon is None:
130
- return []
131
- prefs = addon.preferences
132
- # refresh_devices(), NOT get_devices(): the latter is deprecated on 4.5 and
133
- # returns None, so the ubiquitous `prefs.get_devices()[0]` idiom raises
134
- # TypeError. And prefs.devices is empty until this call.
135
- try:
136
- prefs.refresh_devices()
137
- except Exception as e: # noqa: BLE001
138
- sys.stderr.write("[gpu] refresh_devices failed: %s\n" % e)
139
- return []
140
-
141
- # bl_rna enum_items is EMPTY for this dynamic enum — get_device_types() is
142
- # the only reliable way to ask what this build supports.
143
- try:
144
- available = {t[0] for t in prefs.get_device_types(bpy.context)}
145
- except Exception: # noqa: BLE001
146
- available = set()
147
- # NONE is always offered and means CPU — never treat it as a GPU backend.
148
- available.discard("NONE")
149
-
150
- pinned = os.environ.get("GENEX_BLENDER_CYCLES_BACKEND")
151
- if pinned:
152
- backend = pinned if pinned in available else None
153
- else:
154
- backend = next((b for b in CYCLES_BACKEND_PREFERENCE if b in available), None)
155
- if backend is None:
156
- sys.stderr.write("[gpu] no GPU compute backend offered (have: %s)\n"
157
- % sorted(available))
158
- return []
159
-
160
- prefs.compute_device_type = backend
161
- prefs.refresh_devices()
162
-
163
- gpus = []
164
- for d in prefs.devices:
165
- # NOT `d.use = True`. The CPU is ALWAYS in the candidate list, and new
166
- # GPU entries already default to use=True, so blanket-enabling silently
167
- # turns on hybrid CPU+GPU rendering that still reports as "GPU".
168
- d.use = (d.type == backend)
169
- if d.use:
170
- gpus.append("%s (%s)" % (d.name, backend))
171
-
172
- # Per SCENE, and stored in the .blend — so a restored checkpoint can arrive
173
- # with device='CPU' even when preferences are perfect.
174
- for scene in bpy.data.scenes:
175
- try:
176
- scene.cycles.device = "GPU"
177
- except AttributeError:
178
- pass # cycles addon present but scene not yet initialised
179
- return gpus
180
-
181
-
182
- def witness():
183
- """What the renderer actually is, right now. Cheap enough to call at boot."""
184
- import gpu as bgpu
185
-
186
- probe_ok = _force_context()
187
- try:
188
- return {
189
- "eeveeProbe": probe_ok,
190
- "backend": bgpu.platform.backend_type_get(),
191
- "device": bgpu.platform.device_type_get(),
192
- "renderer": bgpu.platform.renderer_get(),
193
- "vendor": bgpu.platform.vendor_get(),
194
- "glVersion": bgpu.platform.version_get(),
195
- "cyclesDevices": configure_cycles(),
196
- "eeveeEngine": EEVEE_ENGINE,
197
- }
198
- except Exception as e: # noqa: BLE001
199
- return {"eeveeProbe": probe_ok, "backend": "NONE", "device": "UNKNOWN",
200
- "renderer": str(e), "vendor": "", "glVersion": "",
201
- "cyclesDevices": configure_cycles(), "eeveeEngine": EEVEE_ENGINE}
202
-
203
-
204
- def assert_gpu_or_die():
205
- """Refuse to serve on software. Called BEFORE the socket opens, so a
206
- misconfigured GPU pod never accepts a request it would bill for and answer
207
- from the CPU."""
208
- w = witness()
209
- # The verdict rests on INDEPENDENT EVIDENCE, not on gpu.platform alone.
210
- # Blender may decline to answer gpu.platform in background mode even when a
211
- # GPU is present and working, so treating UNKNOWN as fatal would refuse a
212
- # healthy pod and take the whole tier down. Explicit SOFTWARE is fatal; an
213
- # unanswered query is only fatal when the EEVEE probe ALSO produced nothing.
214
- if w["device"] == "SOFTWARE":
215
- sys.stderr.write("[svc] FATAL gpu_tier_on_software: device reported SOFTWARE (%s)\n"
216
- % w["renderer"])
217
- sys.stderr.flush()
218
- sys.exit(3)
219
- if not w["eeveeProbe"]:
220
- sys.stderr.write(
221
- "[svc] FATAL eevee_unavailable: probe render produced nothing "
222
- "(backend=%s device=%s renderer=%r)\n"
223
- % (w["backend"], w["device"], w["renderer"]))
224
- sys.stderr.flush()
225
- sys.exit(3)
226
- if w["device"] in ("UNKNOWN", "NONE"):
227
- # Not fatal, but no longer silent: on a healthy NVIDIA pod this answers
228
- # "NVIDIA", so an unanswered query means the rasterizer path -- the one
229
- # every contact sheet uses -- is unverified.
230
- sys.stderr.write(
231
- "[svc] WARN rasterizer_unverified: gpu.platform reported %s. Cycles "
232
- "has a GPU device, but EEVEE/Workbench GPU use is unconfirmed.\n"
233
- % w["device"])
234
- if not w["cyclesDevices"]:
235
- # A software rasterizer cannot enumerate one. If this fires on a box you
236
- # believe has a GPU, the driver or container toolkit is the problem.
237
- sys.stderr.write("[svc] FATAL no_cycles_device: no GPU compute device "
238
- "enumerated (this is the real GPU gate; EEVEE rendering "
239
- "proves nothing)\n")
240
- sys.stderr.flush()
241
- sys.exit(4)
242
- sys.stderr.write("[svc] GPU ok: %s %s | cycles: %s\n"
243
- % (w["device"], w["renderer"], ", ".join(w["cyclesDevices"])))
244
- sys.stderr.flush()
245
- return w