@genex-ai/cli-demo 1.30.0-dev.644 → 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.
- package/README.md +0 -18
- package/dist/index.js +2839 -4768
- package/package.json +3 -3
- package/templates/controllers/character/follow-camera.ts +1 -16
- package/templates/controllers/character/meshy/meshy-loader.ts +2 -3
- package/templates/controllers/quality/pick-asset.ts +16 -49
- package/templates/controllers/shared/physics-world.ts +4 -6
- package/templates/skills/genex-ai-character/SKILL.md +15 -77
- package/templates/skills/genex-ai-menu/SKILL.md +11 -15
- package/templates/skills/genex-ai-model/SKILL.md +7 -45
- package/templates/skills/genex-ai-texture/SKILL.md +1 -1
- package/templates/skills/genex-ai-video/SKILL.md +17 -75
- package/templates/skills/genex-game-director/SKILL.md +46 -112
- package/templates/skills/genex-game-director/references/design-contract.md +5 -13
- package/templates/skills/genex-game-director/references/routing-map.md +45 -30
- package/templates/skills/genex-getting-started/SKILL.md +2 -2
- package/templates/skills/genex-monetization/SKILL.md +171 -0
- package/templates/skills/genex-threejs-adaptive-quality/SKILL.md +0 -21
- package/templates/skills/genex-threejs-character-controller/SKILL.md +5 -17
- package/templates/skills/genex-threejs-creatures/SKILL.md +1 -8
- package/templates/skills/genex-threejs-embed-auth/SKILL.md +52 -8
- package/templates/skills/genex-threejs-game-ui/SKILL.md +6 -55
- package/templates/skills/genex-threejs-procedural-assets/SKILL.md +10 -17
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +2 -12
- package/templates/skills/genex-tool-audio/SKILL.md +2 -3
- package/templates/skills/genex-tool-character/SKILL.md +5 -36
- package/templates/skills/genex-tool-image/SKILL.md +2 -4
- package/templates/skills/genex-tool-model/SKILL.md +6 -32
- package/templates/skills/genex-tool-texture/SKILL.md +1 -1
- package/templates/skills/genex-tool-video/SKILL.md +5 -28
- package/templates/skills/genex-tool-workflow/SKILL.md +1 -4
- package/templates/skills/genex-updates/SKILL.md +1 -1
- package/dist/blender-mcp-Q6PSFYSE.js +0 -241
- package/dist/blender-serve-BF4FZ55Z.js +0 -244
- package/dist/chunk-2COG4P3T.js +0 -968
- package/dist/chunk-HYCSNWYX.js +0 -126
- package/templates/blender-service/demo/castle.py +0 -117
- package/templates/blender-service/gpu_witness.py +0 -245
- package/templates/blender-service/ops.py +0 -225
- package/templates/blender-service/pool.py +0 -910
- package/templates/blender-service/server.py +0 -611
- package/templates/blender-service/supervisor.py +0 -221
- package/templates/blender-service/views.py +0 -281
- package/templates/controllers/quality/deadline.ts +0 -117
- package/templates/skills/genex-blender-scene/SKILL.md +0 -243
- package/templates/skills/genex-lane-card/SKILL.md +0 -78
- package/templates/skills/genex-tool-publish/SKILL.md +0 -100
package/dist/chunk-HYCSNWYX.js
DELETED
|
@@ -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
|